<?php

namespace PHPPgAdmin\Database;

/**
 * A Class that implements the DB Interface for Postgres
 * Note: This Class uses ADODB and returns RecordSets.
 *
 * $Id: Postgres.php,v 1.320 2008/02/20 20:43:09 ioguix Exp $
 */

class Postgres extends ADOdbBase
{

    use \PHPPgAdmin\HelperTrait;

    public $major_version = 9.6;
    // Max object name length
    public $_maxNameLen = 63;
    // Store the current schema
    public $_schema;
    // Map of database encoding names to HTTP encoding names.  If a
    // database encoding does not appear in this list, then its HTTP
    // encoding name is the same as its database encoding name.
    public $codemap = [
        'BIG5'       => 'BIG5',
        'EUC_CN'     => 'GB2312',
        'EUC_JP'     => 'EUC-JP',
        'EUC_KR'     => 'EUC-KR',
        'EUC_TW'     => 'EUC-TW',
        'GB18030'    => 'GB18030',
        'GBK'        => 'GB2312',
        'ISO_8859_5' => 'ISO-8859-5',
        'ISO_8859_6' => 'ISO-8859-6',
        'ISO_8859_7' => 'ISO-8859-7',
        'ISO_8859_8' => 'ISO-8859-8',
        'JOHAB'      => 'CP1361',
        'KOI8'       => 'KOI8-R',
        'LATIN1'     => 'ISO-8859-1',
        'LATIN2'     => 'ISO-8859-2',
        'LATIN3'     => 'ISO-8859-3',
        'LATIN4'     => 'ISO-8859-4',
        'LATIN5'     => 'ISO-8859-9',
        'LATIN6'     => 'ISO-8859-10',
        'LATIN7'     => 'ISO-8859-13',
        'LATIN8'     => 'ISO-8859-14',
        'LATIN9'     => 'ISO-8859-15',
        'LATIN10'    => 'ISO-8859-16',
        'SJIS'       => 'SHIFT_JIS',
        'SQL_ASCII'  => 'US-ASCII',
        'UHC'        => 'WIN949',
        'UTF8'       => 'UTF-8',
        'WIN866'     => 'CP866',
        'WIN874'     => 'CP874',
        'WIN1250'    => 'CP1250',
        'WIN1251'    => 'CP1251',
        'WIN1252'    => 'CP1252',
        'WIN1256'    => 'CP1256',
        'WIN1258'    => 'CP1258',
    ];
    public $defaultprops = ['', '', ''];
    // Extra "magic" types.  BIGSERIAL was added in PostgreSQL 7.2.
    public $extraTypes = ['SERIAL', 'BIGSERIAL'];
    // Foreign key stuff.  First element MUST be the default.
    public $fkactions    = ['NO ACTION', 'RESTRICT', 'CASCADE', 'SET NULL', 'SET DEFAULT'];
    public $fkdeferrable = ['NOT DEFERRABLE', 'DEFERRABLE'];
    public $fkinitial    = ['INITIALLY IMMEDIATE', 'INITIALLY DEFERRED'];
    public $fkmatches    = ['MATCH SIMPLE', 'MATCH FULL'];
    // Function properties
    public $funcprops = [
        ['', 'VOLATILE', 'IMMUTABLE', 'STABLE'],
        ['', 'CALLED ON NULL INPUT', 'RETURNS NULL ON NULL INPUT'],
        ['', 'SECURITY INVOKER', 'SECURITY DEFINER'],
    ];

    // Default help URL
    public $help_base = null;
    // Help sub pages
    public $help_page = null;
    // Name of id column
    public $id = 'oid';

    // Supported join operations for use with view wizard
    public $joinOps = ['INNER JOIN' => 'INNER JOIN', 'LEFT JOIN' => 'LEFT JOIN', 'RIGHT JOIN' => 'RIGHT JOIN', 'FULL JOIN' => 'FULL JOIN'];
    // Map of internal language name to syntax highlighting name
    public $langmap = [
        'sql'       => 'SQL',
        'plpgsql'   => 'SQL',
        'php'       => 'PHP',
        'phpu'      => 'PHP',
        'plphp'     => 'PHP',
        'plphpu'    => 'PHP',
        'perl'      => 'Perl',
        'perlu'     => 'Perl',
        'plperl'    => 'Perl',
        'plperlu'   => 'Perl',
        'java'      => 'Java',
        'javau'     => 'Java',
        'pljava'    => 'Java',
        'pljavau'   => 'Java',
        'plj'       => 'Java',
        'plju'      => 'Java',
        'python'    => 'Python',
        'pythonu'   => 'Python',
        'plpython'  => 'Python',
        'plpythonu' => 'Python',
        'ruby'      => 'Ruby',
        'rubyu'     => 'Ruby',
        'plruby'    => 'Ruby',
        'plrubyu'   => 'Ruby',
    ];
    // Predefined size types
    public $predefined_size_types = [
        'abstime',
        'aclitem',
        'bigserial',
        'boolean',
        'bytea',
        'cid',
        'cidr',
        'circle',
        'date',
        'float4',
        'float8',
        'gtsvector',
        'inet',
        'int2',
        'int4',
        'int8',
        'macaddr',
        'money',
        'oid',
        'path',
        'polygon',
        'refcursor',
        'regclass',
        'regoper',
        'regoperator',
        'regproc',
        'regprocedure',
        'regtype',
        'reltime',
        'serial',
        'smgr',
        'text',
        'tid',
        'tinterval',
        'tsquery',
        'tsvector',
        'varbit',
        'void',
        'xid',
    ];
    // List of all legal privileges that can be applied to different types
    // of objects.
    public $privlist = [
        'table'      => ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'REFERENCES', 'TRIGGER', 'ALL PRIVILEGES'],
        'view'       => ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'REFERENCES', 'TRIGGER', 'ALL PRIVILEGES'],
        'sequence'   => ['SELECT', 'UPDATE', 'ALL PRIVILEGES'],
        'database'   => ['CREATE', 'TEMPORARY', 'CONNECT', 'ALL PRIVILEGES'],
        'function'   => ['EXECUTE', 'ALL PRIVILEGES'],
        'language'   => ['USAGE', 'ALL PRIVILEGES'],
        'schema'     => ['CREATE', 'USAGE', 'ALL PRIVILEGES'],
        'tablespace' => ['CREATE', 'ALL PRIVILEGES'],
        'column'     => ['SELECT', 'INSERT', 'UPDATE', 'REFERENCES', 'ALL PRIVILEGES'],
    ];
    // List of characters in acl lists and the privileges they
    // refer to.
    public $privmap = [
        'r' => 'SELECT',
        'w' => 'UPDATE',
        'a' => 'INSERT',
        'd' => 'DELETE',
        'D' => 'TRUNCATE',
        'R' => 'RULE',
        'x' => 'REFERENCES',
        't' => 'TRIGGER',
        'X' => 'EXECUTE',
        'U' => 'USAGE',
        'C' => 'CREATE',
        'T' => 'TEMPORARY',
        'c' => 'CONNECT',
    ];
    // Rule action types
    public $rule_events = ['SELECT', 'INSERT', 'UPDATE', 'DELETE'];
    // Select operators
    public $selectOps = [
        '='                   => 'i',
        '!='                  => 'i',
        '<'                   => 'i',
        '>'                   => 'i',
        '<='                  => 'i',
        '>='                  => 'i',
        '<<'                  => 'i',
        '>>'                  => 'i',
        '<<='                 => 'i',
        '>>='                 => 'i',
        'LIKE'                => 'i',
        'NOT LIKE'            => 'i',
        'ILIKE'               => 'i',
        'NOT ILIKE'           => 'i',
        'SIMILAR TO'          => 'i',
        'NOT SIMILAR TO'      => 'i',
        '~'                   => 'i',
        '!~'                  => 'i',
        '~*'                  => 'i',
        '!~*'                 => 'i',
        'IS NULL'             => 'p',
        'IS NOT NULL'         => 'p',
        'IN'                  => 'x',
        'NOT IN'              => 'x',
        '@@'                  => 'i',
        '@@@'                 => 'i',
        '@>'                  => 'i',
        '<@'                  => 'i',
        '@@ to_tsquery'       => 't',
        '@@@ to_tsquery'      => 't',
        '@> to_tsquery'       => 't',
        '<@ to_tsquery'       => 't',
        '@@ plainto_tsquery'  => 't',
        '@@@ plainto_tsquery' => 't',
        '@> plainto_tsquery'  => 't',
        '<@ plainto_tsquery'  => 't',
    ];
    // Array of allowed trigger events
    public $triggerEvents = [
        'INSERT',
        'UPDATE',
        'DELETE',
        'INSERT OR UPDATE',
        'INSERT OR DELETE',
        'DELETE OR UPDATE',
        'INSERT OR DELETE OR UPDATE',
    ];
    // When to execute the trigger
    public $triggerExecTimes = ['BEFORE', 'AFTER'];
    // How often to execute the trigger
    public $triggerFrequency = ['ROW', 'STATEMENT'];
    // Array of allowed type alignments
    public $typAligns = ['char', 'int2', 'int4', 'double'];
    // The default type alignment
    public $typAlignDef = 'int4';
    // Default index type
    public $typIndexDef = 'BTREE';
    // Array of allowed index types
    public $typIndexes = ['BTREE', 'RTREE', 'GIST', 'GIN', 'HASH'];
    // Array of allowed type storage attributes
    public $typStorages = ['plain', 'external', 'extended', 'main'];
    // The default type storage
    public $typStorageDef = 'plain';

    public function __construct(&$conn, $conf)
    {
        //$this->prtrace('major_version :' . $this->major_version);
        $this->conn = $conn;
        $this->conf = $conf;
    }

    /**
     * Fetch a URL (or array of URLs) for a given help page.
     *
     * @param $help
     * @return array|null|string
     */
    public function getHelp($help)
    {
        $this->getHelpPages();

        if (isset($this->help_page[$help])) {
            if (is_array($this->help_page[$help])) {
                $urls = [];
                foreach ($this->help_page[$help] as $link) {
                    $urls[] = $this->help_base . $link;
                }

                return $urls;
            }

            return $this->help_base . $this->help_page[$help];
        } else {
            return null;
        }
    }

    /**
     * get help page by instancing the corresponding help class
     * if $this->help_page and $this->help_base are set, this function is a noop
     */
    public function getHelpPages()
    {
        if ($this->help_page === null || $this->help_base === null) {
            $help_classname = '\PHPPgAdmin\Help\PostgresDoc' . str_replace('.', '', $this->major_version);

            $help_class = new $help_classname($this->conf, $this->major_version);

            $this->help_base = $help_class->getHelpBase();
            $this->help_page = $help_class->getHelpPage();
        }
    }

    // Formatting functions

    /**
     * Outputs the HTML code for a particular field
     *
     * @param                               $name   The name to give the field
     * @param                               $value  The value of the field.  Note this could be 'numeric(7,2)' sort of thing...
     * @param                               $type   The database type of the field
     * @param array|\PHPPgAdmin\Database\An $extras An array of attributes name as key and attributes' values as value
     */
    public function printField($name, $value, $type, $extras = [])
    {
        $lang = $this->lang;

        // Determine actions string
        $extra_str = '';
        foreach ($extras as $k => $v) {
            $extra_str .= " {$k}=\"" . htmlspecialchars($v) . '"';
        }

        switch (substr($type, 0, 9)) {
            case 'bool':
            case 'boolean':
                if ($value !== null && $value == '') {
                    $value = null;
                } elseif ($value == 'true') {
                    $value = 't';
                } elseif ($value == 'false') {
                    $value = 'f';
                }

                // If value is null, 't' or 'f'...
                if ($value === null || $value == 't' || $value == 'f') {
                    echo '<select name="', htmlspecialchars($name), "\"{$extra_str}>\n";
                    echo '<option value=""', ($value === null) ? ' selected="selected"' : '', "></option>\n";
                    echo '<option value="t"', ($value == 't') ? ' selected="selected"' : '', ">{$lang['strtrue']}</option>\n";
                    echo '<option value="f"', ($value == 'f') ? ' selected="selected"' : '', ">{$lang['strfalse']}</option>\n";
                    echo "</select>\n";
                } else {
                    echo '<input name="', htmlspecialchars($name), '" value="', htmlspecialchars($value), "\" size=\"35\"{$extra_str} />\n";
                }
                break;
            case 'bytea':
            case 'bytea[]':
                if (!is_null($value)) {
                    $value = $this->escapeBytea($value);
                }
            case 'text':
            case 'text[]':
            case 'json':
            case 'jsonb':
            case 'xml':
            case 'xml[]':
                $n = substr_count($value, "\n");
                $n = $n < 5 ? max(2, $n) : $n;
                $n = $n > 20 ? 20 : $n;
                echo '<textarea name="', htmlspecialchars($name), "\" rows=\"{$n}\" cols=\"85\"{$extra_str}>\n";
                echo htmlspecialchars($value);
                echo "</textarea>\n";
                break;
            case 'character':
            case 'character[]':
                $n = substr_count($value, "\n");
                $n = $n < 5 ? 5 : $n;
                $n = $n > 20 ? 20 : $n;
                echo '<textarea name="', htmlspecialchars($name), "\" rows=\"{$n}\" cols=\"35\"{$extra_str}>\n";
                echo htmlspecialchars($value);
                echo "</textarea>\n";
                break;
            default:
                echo '<input name="', htmlspecialchars($name), '" value="', htmlspecialchars($value), "\" size=\"35\"{$extra_str} />\n";
                break;
        }
    }

    /**
     * Escapes bytea data for display on the screen
     *
     * @param $data The bytea data
     * @return Data formatted for on-screen display
     */
    public function escapeBytea($data)
    {
        return htmlentities($data, ENT_QUOTES, 'UTF-8');
    }

    /**
     * Return all information about a particular database
     *
     * @param $database The name of the database to retrieve
     * @return The database info
     */
    public function getDatabase($database)
    {
        $this->clean($database);
        $sql = "SELECT * FROM pg_database WHERE datname='{$database}'";

        return $this->selectSet($sql);
    }

    /**
     * Cleans (escapes) a string
     *
     * @param $str The string to clean, by reference
     * @return The cleaned string
     */
    public function clean(&$str)
    {
        if ($str === null) {
            return null;
        }

        $str = str_replace("\r\n", "\n", $str);
        $str = pg_escape_string($str);

        return $str;
    }

    /**
     * Return all database available on the server
     *
     * @param $currentdatabase database name that should be on top of the resultset
     *
     * @return A list of databases, sorted alphabetically
     */
    public function getDatabases($currentdatabase = null)
    {
        $conf        = $this->conf;
        $server_info = $this->server_info;

        if (isset($conf['owned_only']) && $conf['owned_only'] && !$this->isSuperUser()) {
            $username = $server_info['username'];
            $this->clean($username);
            $clause = " AND pr.rolname='{$username}'";
        } else {
            $clause = '';
        }
        if (isset($server_info['useonlydefaultdb']) && $server_info['useonlydefaultdb']) {
            $currentdatabase = $server_info['defaultdb'];
            $clause .= " AND pdb.datname = '{$currentdatabase}' ";
        }

        if (isset($server_info['hiddendbs']) && $server_info['hiddendbs']) {
            $hiddendbs = $server_info['hiddendbs'];
            $not_in    = "('" . implode("','", $hiddendbs) . "')";
            $clause .= " AND pdb.datname NOT IN {$not_in} ";
        }

        if ($currentdatabase != null) {
            $this->clean($currentdatabase);
            $orderby = "ORDER BY pdb.datname = '{$currentdatabase}' DESC, pdb.datname";
        } else {
            $orderby = 'ORDER BY pdb.datname';
        }

        if (!$conf['show_system']) {
            $where = ' AND NOT pdb.datistemplate';
        } else {
            $where = ' AND pdb.datallowconn';
        }

        $sql = "
			SELECT pdb.datname AS datname,
                    pr.rolname AS datowner,
                    pg_encoding_to_char(encoding) AS datencoding,
				    (SELECT description FROM pg_catalog.pg_shdescription pd WHERE pdb.oid=pd.objoid AND pd.classoid='pg_database'::regclass) AS datcomment,
				    (SELECT spcname FROM pg_catalog.pg_tablespace pt WHERE pt.oid=pdb.dattablespace) AS tablespace,
				CASE WHEN pg_catalog.has_database_privilege(current_user, pdb.oid, 'CONNECT')
					THEN pg_catalog.pg_database_size(pdb.oid)
					ELSE -1 -- set this magic value, which we will convert to no access later
				END as dbsize,
                pdb.datcollate,
                pdb.datctype
			FROM pg_catalog.pg_database pdb
            LEFT JOIN pg_catalog.pg_roles pr ON (pdb.datdba = pr.oid)
			WHERE true
				{$where}
				{$clause}
			{$orderby}";

        return $this->selectSet($sql);
    }

    /**
     * Determines whether or not a user is a super user
     *
     * @param \PHPPgAdmin\Database\The|string $username The username of the user
     * @return True if is a super user, false otherwise
     */
    public function isSuperUser($username = '')
    {
        $this->clean($username);

        if (empty($usename)) {
            $val = pg_parameter_status($this->conn->_connectionID, 'is_superuser');
            if ($val !== false) {
                return $val == 'on';
            }
        }

        $sql = "SELECT usesuper FROM pg_user WHERE usename='{$username}'";

        $usesuper = $this->selectField($sql, 'usesuper');
        if ($usesuper == -1) {
            return false;
        }

        return $usesuper == 't';
    }

    /**
     * Return the database comment of a db from the shared description table
     *
     * @param string $database the name of the database to get the comment for
     * @return recordset of the db comment info
     */
    public function getDatabaseComment($database)
    {
        $this->clean($database);
        $sql =
            "SELECT description FROM pg_catalog.pg_database JOIN pg_catalog.pg_shdescription ON (oid=objoid AND classoid='pg_database'::regclass) WHERE pg_database.datname = '{$database}' ";

        return $this->selectSet($sql);
    }

    /**
     * Return the database owner of a db
     *
     * @param string $database the name of the database to get the owner for
     * @return recordset of the db owner info
     */
    public function getDatabaseOwner($database)
    {
        $this->clean($database);
        $sql = "SELECT usename FROM pg_user, pg_database WHERE pg_user.usesysid = pg_database.datdba AND pg_database.datname = '{$database}' ";

        return $this->selectSet($sql);
    }

    // Help functions

    // Database functions

    /**
     * Returns the current database encoding
     *
     * @return The encoding.  eg. SQL_ASCII, UTF-8, etc.
     */
    public function getDatabaseEncoding()
    {
        return pg_parameter_status($this->conn->_connectionID, 'server_encoding');
    }

    /**
     * Returns the current default_with_oids setting
     *
     * @return default_with_oids setting
     */
    public function getDefaultWithOid()
    {

        $sql = 'SHOW default_with_oids';

        return $this->selectField($sql, 'default_with_oids');
    }

    /**
     * Creates a database
     *
     * @param        $database   The name of the database to create
     * @param        $encoding   Encoding of the database
     * @param string $tablespace (optional) The tablespace name
     * @param string $comment
     * @param string $template
     * @param string $lc_collate
     * @param string $lc_ctype
     * @return int 0 success
     */
    public function createDatabase(
        $database,
        $encoding,
        $tablespace = '',
        $comment = '',
        $template = 'template1',
        $lc_collate = '',
        $lc_ctype = ''
    ) {
        $this->fieldClean($database);
        $this->clean($encoding);
        $this->fieldClean($tablespace);
        $this->fieldClean($template);
        $this->clean($lc_collate);
        $this->clean($lc_ctype);

        $sql = "CREATE DATABASE \"{$database}\" WITH TEMPLATE=\"{$template}\"";

        if ($encoding != '') {
            $sql .= " ENCODING='{$encoding}'";
        }

        if ($lc_collate != '') {
            $sql .= " LC_COLLATE='{$lc_collate}'";
        }

        if ($lc_ctype != '') {
            $sql .= " LC_CTYPE='{$lc_ctype}'";
        }

        if ($tablespace != '' && $this->hasTablespaces()) {
            $sql .= " TABLESPACE \"{$tablespace}\"";
        }

        $status = $this->execute($sql);
        if ($status != 0) {
            return -1;
        }

        if ($comment != '' && $this->hasSharedComments()) {
            $status = $this->setComment('DATABASE', $database, '', $comment);
            if ($status != 0) {
                return -2;
            }
        }

        return 0;
    }

    /**
     * Cleans (escapes) an object name (eg. table, field)
     *
     * @param $str The string to clean, by reference
     * @return The cleaned string
     */
    public function fieldClean(&$str)
    {
        if ($str === null) {
            return null;
        }

        $str = str_replace('"', '""', $str);

        return $str;
    }

    public function hasTablespaces()
    {
        return true;
    }

    public function hasSharedComments()
    {
        return true;
    }

    /**
     * Sets the comment for an object in the database
     *
     * @pre All parameters must already be cleaned
     * @param      $obj_type One of 'TABLE' | 'COLUMN' | 'VIEW' | 'SCHEMA' | 'SEQUENCE' | 'TYPE' | 'FUNCTION' | 'AGGREGATE'
     * @param      $obj_name The name of the object for which to attach a comment.
     * @param      $table    Name of table that $obj_name belongs to.  Ignored unless $obj_type is 'TABLE' or 'COLUMN'.
     * @param      $comment  The comment to add.
     * @param null $basetype
     * @return int|\PHPPgAdmin\Database\A 0 success
     */
    public function setComment($obj_type, $obj_name, $table, $comment, $basetype = null)
    {
        $sql      = "COMMENT ON {$obj_type} ";
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->clean($comment); // Passing in an already cleaned comment will lead to double escaped data
        // So, while counter-intuitive, it is important to not clean comments before
        // calling setComment. We will clean it here instead.
        /*
        $this->fieldClean($table);
        $this->fieldClean($obj_name);
         */

        switch ($obj_type) {
            case 'TABLE':
                $sql .= "\"{$f_schema}\".\"{$table}\" IS ";
                break;
            case 'COLUMN':
                $sql .= "\"{$f_schema}\".\"{$table}\".\"{$obj_name}\" IS ";
                break;
            case 'SEQUENCE':
            case 'VIEW':
            case 'TEXT SEARCH CONFIGURATION':
            case 'TEXT SEARCH DICTIONARY':
            case 'TEXT SEARCH TEMPLATE':
            case 'TEXT SEARCH PARSER':
            case 'TYPE':
                $sql .= "\"{$f_schema}\".";
            case 'DATABASE':
            case 'ROLE':
            case 'SCHEMA':
            case 'TABLESPACE':
                $sql .= "\"{$obj_name}\" IS ";
                break;
            case 'FUNCTION':
                $sql .= "\"{$f_schema}\".{$obj_name} IS ";
                break;
            case 'AGGREGATE':
                $sql .= "\"{$f_schema}\".\"{$obj_name}\" (\"{$basetype}\") IS ";
                break;
            default:
                // Unknown object type
                return -1;
        }

        if ($comment != '') {
            $sql .= "'{$comment}';";
        } else {
            $sql .= 'NULL;';
        }

        return $this->execute($sql);
    }

    /**
     * Drops a database
     *
     * @param $database The name of the database to drop
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function dropDatabase($database)
    {
        $this->fieldClean($database);
        $sql = "DROP DATABASE \"{$database}\"";

        return $this->execute($sql);
    }

    /**
     * Alters a database
     * the multiple return vals are for postgres 8+ which support more functionality in alter database
     *
     * @param                                 $dbName   The name of the database
     * @param                                 $newName  new name for the database
     * @param \PHPPgAdmin\Database\The|string $newOwner The new owner for the database
     * @param string                          $comment
     * @return bool|int 0 success
     */
    public function alterDatabase($dbName, $newName, $newOwner = '', $comment = '')
    {

        $status = $this->beginTransaction();
        if ($status != 0) {
            $this->rollbackTransaction();

            return -1;
        }

        if ($dbName != $newName) {
            $status = $this->alterDatabaseRename($dbName, $newName);
            if ($status != 0) {
                $this->rollbackTransaction();

                return -3;
            }
            $dbName = $newName;
        }

        if ($newOwner != '') {
            $status = $this->alterDatabaseOwner($newName, $newOwner);
            if ($status != 0) {
                $this->rollbackTransaction();

                return -2;
            }
        }

        $this->fieldClean($dbName);
        $status = $this->setComment('DATABASE', $dbName, '', $comment);
        if ($status != 0) {
            $this->rollbackTransaction();

            return -4;
        }

        return $this->endTransaction();
    }

    /**
     * Renames a database, note that this operation cannot be
     * performed on a database that is currently being connected to
     *
     * @param string $oldName name of database to rename
     * @param string $newName new name of database
     * @return int 0 on success
     */
    public function alterDatabaseRename($oldName, $newName)
    {
        $this->fieldClean($oldName);
        $this->fieldClean($newName);

        if ($oldName != $newName) {
            $sql = "ALTER DATABASE \"{$oldName}\" RENAME TO \"{$newName}\"";

            return $this->execute($sql);
        }

        return 0;
    }

    /**
     * Changes ownership of a database
     * This can only be done by a superuser or the owner of the database
     *
     * @param string $dbName   database to change ownership of
     * @param string $newOwner user that will own the database
     * @return int 0 on success
     */
    public function alterDatabaseOwner($dbName, $newOwner)
    {
        $this->fieldClean($dbName);
        $this->fieldClean($newOwner);

        $sql = "ALTER DATABASE \"{$dbName}\" OWNER TO \"{$newOwner}\"";

        return $this->execute($sql);
    }

    /**
     * Returns prepared transactions information
     *
     * @param $database (optional) Find only prepared transactions executed in a specific database
     * @return A recordset
     */
    public function getPreparedXacts($database = null)
    {
        if ($database === null) {
            $sql = 'SELECT * FROM pg_prepared_xacts';
        } else {
            $this->clean($database);
            $sql = "SELECT transaction, gid, prepared, owner FROM pg_prepared_xacts
				WHERE database='{$database}' ORDER BY owner";
        }

        return $this->selectSet($sql);
    }

    /**
     * Searches all system catalogs to find objects that match a certain name.
     *
     * @param $term   The search term
     * @param $filter The object type to restrict to ('' means no restriction)
     * @return A recordset
     */
    public function findObject($term, $filter)
    {
        $conf = $this->conf;

        /*about escaping:
         * SET standard_conforming_string is not available before 8.2
         * So we must use PostgreSQL specific notation :/
         * E'' notation is not available before 8.1
         * $$ is available since 8.0
         * Nothing specific from 7.4
         */

        // Escape search term for ILIKE match
        $this->clean($term);
        $this->clean($filter);
        $term = str_replace('_', '\_', $term);
        $term = str_replace('%', '\%', $term);

        // Exclude system relations if necessary
        if (!$conf['show_system']) {
            // XXX: The mention of information_schema here is in the wrong place, but
            // it's the quickest fix to exclude the info schema from 7.4
            $where     = " AND pn.nspname NOT LIKE \$_PATERN_\$pg\_%\$_PATERN_\$ AND pn.nspname != 'information_schema'";
            $lan_where = 'AND pl.lanispl';
        } else {
            $where     = '';
            $lan_where = '';
        }

        // Apply outer filter
        $sql = '';
        if ($filter != '') {
            $sql = 'SELECT * FROM (';
        }

        $term = "\$_PATERN_\$%{$term}%\$_PATERN_\$";

        $sql .= "
			SELECT 'SCHEMA' AS type, oid, NULL AS schemaname, NULL AS relname, nspname AS name
				FROM pg_catalog.pg_namespace pn WHERE nspname ILIKE {$term} {$where}
			UNION ALL
			SELECT CASE WHEN relkind='r' THEN 'TABLE' WHEN relkind='v' THEN 'VIEW' WHEN relkind='S' THEN 'SEQUENCE' END, pc.oid,
				pn.nspname, NULL, pc.relname FROM pg_catalog.pg_class pc, pg_catalog.pg_namespace pn
				WHERE pc.relnamespace=pn.oid AND relkind IN ('r', 'v', 'S') AND relname ILIKE {$term} {$where}
			UNION ALL
			SELECT CASE WHEN pc.relkind='r' THEN 'COLUMNTABLE' ELSE 'COLUMNVIEW' END, NULL, pn.nspname, pc.relname, pa.attname FROM pg_catalog.pg_class pc, pg_catalog.pg_namespace pn,
				pg_catalog.pg_attribute pa WHERE pc.relnamespace=pn.oid AND pc.oid=pa.attrelid
				AND pa.attname ILIKE {$term} AND pa.attnum > 0 AND NOT pa.attisdropped AND pc.relkind IN ('r', 'v') {$where}
			UNION ALL
			SELECT 'FUNCTION', pp.oid, pn.nspname, NULL, pp.proname || '(' || pg_catalog.oidvectortypes(pp.proargtypes) || ')' FROM pg_catalog.pg_proc pp, pg_catalog.pg_namespace pn
				WHERE pp.pronamespace=pn.oid AND NOT pp.proisagg AND pp.proname ILIKE {$term} {$where}
			UNION ALL
			SELECT 'INDEX', NULL, pn.nspname, pc.relname, pc2.relname FROM pg_catalog.pg_class pc, pg_catalog.pg_namespace pn,
				pg_catalog.pg_index pi, pg_catalog.pg_class pc2 WHERE pc.relnamespace=pn.oid AND pc.oid=pi.indrelid
				AND pi.indexrelid=pc2.oid
				AND NOT EXISTS (
					SELECT 1 FROM pg_catalog.pg_depend d JOIN pg_catalog.pg_constraint c
					ON (d.refclassid = c.tableoid AND d.refobjid = c.oid)
					WHERE d.classid = pc2.tableoid AND d.objid = pc2.oid AND d.deptype = 'i' AND c.contype IN ('u', 'p')
				)
				AND pc2.relname ILIKE {$term} {$where}
			UNION ALL
			SELECT 'CONSTRAINTTABLE', NULL, pn.nspname, pc.relname, pc2.conname FROM pg_catalog.pg_class pc, pg_catalog.pg_namespace pn,
				pg_catalog.pg_constraint pc2 WHERE pc.relnamespace=pn.oid AND pc.oid=pc2.conrelid AND pc2.conrelid != 0
				AND CASE WHEN pc2.contype IN ('f', 'c') THEN TRUE ELSE NOT EXISTS (
					SELECT 1 FROM pg_catalog.pg_depend d JOIN pg_catalog.pg_constraint c
					ON (d.refclassid = c.tableoid AND d.refobjid = c.oid)
					WHERE d.classid = pc2.tableoid AND d.objid = pc2.oid AND d.deptype = 'i' AND c.contype IN ('u', 'p')
				) END
				AND pc2.conname ILIKE {$term} {$where}
			UNION ALL
			SELECT 'CONSTRAINTDOMAIN', pt.oid, pn.nspname, pt.typname, pc.conname FROM pg_catalog.pg_type pt, pg_catalog.pg_namespace pn,
				pg_catalog.pg_constraint pc WHERE pt.typnamespace=pn.oid AND pt.oid=pc.contypid AND pc.contypid != 0
				AND pc.conname ILIKE {$term} {$where}
			UNION ALL
			SELECT 'TRIGGER', NULL, pn.nspname, pc.relname, pt.tgname FROM pg_catalog.pg_class pc, pg_catalog.pg_namespace pn,
				pg_catalog.pg_trigger pt WHERE pc.relnamespace=pn.oid AND pc.oid=pt.tgrelid
					AND ( pt.tgconstraint = 0 OR NOT EXISTS
					(SELECT 1 FROM pg_catalog.pg_depend d JOIN pg_catalog.pg_constraint c
					ON (d.refclassid = c.tableoid AND d.refobjid = c.oid)
					WHERE d.classid = pt.tableoid AND d.objid = pt.oid AND d.deptype = 'i' AND c.contype = 'f'))
				AND pt.tgname ILIKE {$term} {$where}
			UNION ALL
			SELECT 'RULETABLE', NULL, pn.nspname AS schemaname, c.relname AS tablename, r.rulename FROM pg_catalog.pg_rewrite r
				JOIN pg_catalog.pg_class c ON c.oid = r.ev_class
				LEFT JOIN pg_catalog.pg_namespace pn ON pn.oid = c.relnamespace
				WHERE c.relkind='r' AND r.rulename != '_RETURN' AND r.rulename ILIKE {$term} {$where}
			UNION ALL
			SELECT 'RULEVIEW', NULL, pn.nspname AS schemaname, c.relname AS tablename, r.rulename FROM pg_catalog.pg_rewrite r
				JOIN pg_catalog.pg_class c ON c.oid = r.ev_class
				LEFT JOIN pg_catalog.pg_namespace pn ON pn.oid = c.relnamespace
				WHERE c.relkind='v' AND r.rulename != '_RETURN' AND r.rulename ILIKE {$term} {$where}
		";

        // Add advanced objects if show_advanced is set
        if ($conf['show_advanced']) {
            $sql .= "
				UNION ALL
				SELECT CASE WHEN pt.typtype='d' THEN 'DOMAIN' ELSE 'TYPE' END, pt.oid, pn.nspname, NULL,
					pt.typname FROM pg_catalog.pg_type pt, pg_catalog.pg_namespace pn
					WHERE pt.typnamespace=pn.oid AND typname ILIKE {$term}
					AND (pt.typrelid = 0 OR (SELECT c.relkind = 'c' FROM pg_catalog.pg_class c WHERE c.oid = pt.typrelid))
					{$where}
			 	UNION ALL
				SELECT 'OPERATOR', po.oid, pn.nspname, NULL, po.oprname FROM pg_catalog.pg_operator po, pg_catalog.pg_namespace pn
					WHERE po.oprnamespace=pn.oid AND oprname ILIKE {$term} {$where}
				UNION ALL
				SELECT 'CONVERSION', pc.oid, pn.nspname, NULL, pc.conname FROM pg_catalog.pg_conversion pc,
					pg_catalog.pg_namespace pn WHERE pc.connamespace=pn.oid AND conname ILIKE {$term} {$where}
				UNION ALL
				SELECT 'LANGUAGE', pl.oid, NULL, NULL, pl.lanname FROM pg_catalog.pg_language pl
					WHERE lanname ILIKE {$term} {$lan_where}
				UNION ALL
				SELECT DISTINCT ON (p.proname) 'AGGREGATE', p.oid, pn.nspname, NULL, p.proname FROM pg_catalog.pg_proc p
					LEFT JOIN pg_catalog.pg_namespace pn ON p.pronamespace=pn.oid
					WHERE p.proisagg AND p.proname ILIKE {$term} {$where}
				UNION ALL
				SELECT DISTINCT ON (po.opcname) 'OPCLASS', po.oid, pn.nspname, NULL, po.opcname FROM pg_catalog.pg_opclass po,
					pg_catalog.pg_namespace pn WHERE po.opcnamespace=pn.oid
					AND po.opcname ILIKE {$term} {$where}
			";
        } // Otherwise just add domains
        else {
            $sql .= "
				UNION ALL
				SELECT 'DOMAIN', pt.oid, pn.nspname, NULL,
					pt.typname FROM pg_catalog.pg_type pt, pg_catalog.pg_namespace pn
					WHERE pt.typnamespace=pn.oid AND pt.typtype='d' AND typname ILIKE {$term}
					AND (pt.typrelid = 0 OR (SELECT c.relkind = 'c' FROM pg_catalog.pg_class c WHERE c.oid = pt.typrelid))
					{$where}
			";
        }

        if ($filter != '') {
            // We use like to make RULE, CONSTRAINT and COLUMN searches work
            $sql .= ") AS sub WHERE type LIKE '{$filter}%' ";
        }

        $sql .= 'ORDER BY type, schemaname, relname, name';

        return $this->selectSet($sql);
    }

    /**
     * Returns all available variable information.
     *
     * @return A recordset
     */
    public function getVariables()
    {
        $sql = 'SHOW ALL';

        return $this->selectSet($sql);
    }

    // Schema functons

    /**
     * Return all schemas in the current database.
     *
     * @return All schemas, sorted alphabetically
     */
    public function getSchemas()
    {
        $conf = $this->conf;

        if (!$conf['show_system']) {
            $where = "WHERE nspname NOT LIKE 'pg@_%' ESCAPE '@' AND nspname != 'information_schema'";
        } else {
            $where = "WHERE nspname !~ '^pg_t(emp_[0-9]+|oast)$'";
        }

        $sql = "
			SELECT pn.nspname, pu.rolname AS nspowner,
				pg_catalog.obj_description(pn.oid, 'pg_namespace') AS nspcomment
			FROM pg_catalog.pg_namespace pn
				LEFT JOIN pg_catalog.pg_roles pu ON (pn.nspowner = pu.oid)
			{$where}
			ORDER BY nspname";

        return $this->selectSet($sql);
    }

    /**
     * Sets the current working schema.  Will also set Class variable.
     *
     * @param $schema The the name of the schema to work in
     * @return int|\PHPPgAdmin\Database\A 0 success
     */
    public function setSchema($schema)
    {
        // Get the current schema search path, including 'pg_catalog'.
        $search_path = $this->getSearchPath();
        // Prepend $schema to search path
        array_unshift($search_path, $schema);
        $status = $this->setSearchPath($search_path);
        if ($status == 0) {
            $this->_schema = $schema;

            return 0;
        }

        return $status;
    }

    /**
     * Return the current schema search path
     *
     * @return Array of schema names
     */
    public function getSearchPath()
    {
        $sql = 'SELECT current_schemas(false) AS search_path';

        return $this->phpArray($this->selectField($sql, 'search_path'));
    }

    /**
     * Sets the current schema search path
     *
     * @param $paths An array of schemas in required search order
     * @return int|\PHPPgAdmin\Database\A 0 success
     */
    public function setSearchPath($paths)
    {
        if (!is_array($paths)) {
            return -1;
        }

        if (sizeof($paths) == 0) {
            return -2;
        } elseif (sizeof($paths) == 1 && $paths[0] == '') {
            // Need to handle empty paths in some cases
            $paths[0] = 'pg_catalog';
        }

        // Loop over all the paths to check that none are empty
        $temp = [];
        foreach ($paths as $schema) {
            if ($schema != '') {
                $temp[] = $schema;
            }
        }
        $this->fieldArrayClean($temp);

        $sql = 'SET SEARCH_PATH TO "' . implode('","', $temp) . '"';

        return $this->execute($sql);
    }

    /**
     * Cleans (escapes) an array of field names
     *
     * @param $arr The array to clean, by reference
     * @return The cleaned array
     */
    public function fieldArrayClean(&$arr)
    {
        foreach ($arr as $k => $v) {
            if ($v === null) {
                continue;
            }

            $arr[$k] = str_replace('"', '""', $v);
        }

        return $arr;
    }

    /**
     * Creates a new schema.
     *
     * @param        $schemaname    The name of the schema to create
     * @param string $authorization (optional) The username to create the schema for.
     * @param string $comment       (optional) If omitted, defaults to nothing
     * @return bool|int 0 success
     */
    public function createSchema($schemaname, $authorization = '', $comment = '')
    {
        $this->fieldClean($schemaname);
        $this->fieldClean($authorization);

        $sql = "CREATE SCHEMA \"{$schemaname}\"";
        if ($authorization != '') {
            $sql .= " AUTHORIZATION \"{$authorization}\"";
        }

        if ($comment != '') {
            $status = $this->beginTransaction();
            if ($status != 0) {
                return -1;
            }
        }

        // Create the new schema
        $status = $this->execute($sql);
        if ($status != 0) {
            $this->rollbackTransaction();

            return -1;
        }

        // Set the comment
        if ($comment != '') {
            $status = $this->setComment('SCHEMA', $schemaname, '', $comment);
            if ($status != 0) {
                $this->rollbackTransaction();

                return -1;
            }

            return $this->endTransaction();
        }

        return 0;
    }

    /**
     * Updates a schema.
     *
     * @param $schemaname The name of the schema to drop
     * @param $comment    The new comment for this schema
     * @param $name
     * @param $owner      The new owner for this schema
     * @return bool|int 0 success
     */
    public function updateSchema($schemaname, $comment, $name, $owner)
    {
        $this->fieldClean($schemaname);
        $this->fieldClean($name);
        $this->fieldClean($owner);

        $status = $this->beginTransaction();
        if ($status != 0) {
            $this->rollbackTransaction();

            return -1;
        }

        $status = $this->setComment('SCHEMA', $schemaname, '', $comment);
        if ($status != 0) {
            $this->rollbackTransaction();

            return -1;
        }

        $schema_rs = $this->getSchemaByName($schemaname);
        /* Only if the owner change */
        if ($schema_rs->fields['ownername'] != $owner) {
            $sql    = "ALTER SCHEMA \"{$schemaname}\" OWNER TO \"{$owner}\"";
            $status = $this->execute($sql);
            if ($status != 0) {
                $this->rollbackTransaction();

                return -1;
            }
        }

        // Only if the name has changed
        if ($name != $schemaname) {
            $sql    = "ALTER SCHEMA \"{$schemaname}\" RENAME TO \"{$name}\"";
            $status = $this->execute($sql);
            if ($status != 0) {
                $this->rollbackTransaction();

                return -1;
            }
        }

        return $this->endTransaction();
    }

    /**
     * Return all information relating to a schema
     *
     * @param $schema The name of the schema
     * @return Schema information
     */
    public function getSchemaByName($schema)
    {
        $this->clean($schema);
        $sql = "
			SELECT nspname, nspowner, r.rolname AS ownername, nspacl,
				pg_catalog.obj_description(pn.oid, 'pg_namespace') as nspcomment
			FROM pg_catalog.pg_namespace pn
				LEFT JOIN pg_roles as r ON pn.nspowner = r.oid
			WHERE nspname='{$schema}'";

        return $this->selectSet($sql);
    }

    // Table functions

    /**
     * Drops a schema.
     *
     * @param $schemaname The name of the schema to drop
     * @param $cascade    True to cascade drop, false to restrict
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function dropSchema($schemaname, $cascade)
    {
        $this->fieldClean($schemaname);

        $sql = "DROP SCHEMA \"{$schemaname}\"";
        if ($cascade) {
            $sql .= ' CASCADE';
        }

        return $this->execute($sql);
    }

    /**
     * Return all tables in current database (and schema)
     *
     * @param bool|True $all True to fetch all tables, false for just in current schema
     * @return \PHPPgAdmin\Database\All tables, sorted alphabetically
     */
    public function getTables($all = false)
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        if ($all) {
            // Exclude pg_catalog and information_schema tables
            $sql = "SELECT schemaname AS nspname, tablename AS relname, tableowner AS relowner
					FROM pg_catalog.pg_tables
					WHERE schemaname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
					ORDER BY schemaname, tablename";
        } else {
            $sql = "SELECT c.relname, pg_catalog.pg_get_userbyid(c.relowner) AS relowner,
						pg_catalog.obj_description(c.oid, 'pg_class') AS relcomment,
						reltuples::bigint,
						(SELECT spcname FROM pg_catalog.pg_tablespace pt WHERE pt.oid=c.reltablespace) AS tablespace
					FROM pg_catalog.pg_class c
					LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
					WHERE c.relkind = 'r'
					AND nspname='{$c_schema}'
					ORDER BY c.relname";
        }

        return $this->selectSet($sql);
    }

    /**
     * Finds the names and schemas of parent tables (in order)
     *
     * @param $table The table to find the parents for
     * @return A recordset
     */
    public function getTableParents($table)
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $this->clean($table);

        $sql = "
			SELECT
				pn.nspname, relname
			FROM
				pg_catalog.pg_class pc, pg_catalog.pg_inherits pi, pg_catalog.pg_namespace pn
			WHERE
				pc.oid=pi.inhparent
				AND pc.relnamespace=pn.oid
				AND pi.inhrelid = (SELECT oid from pg_catalog.pg_class WHERE relname='{$table}'
					AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = '{$c_schema}'))
			ORDER BY
				pi.inhseqno
		";

        return $this->selectSet($sql);
    }

    /**
     * Finds the names and schemas of child tables
     *
     * @param $table The table to find the children for
     * @return A recordset
     */
    public function getTableChildren($table)
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $this->clean($table);

        $sql = "
			SELECT
				pn.nspname, relname
			FROM
				pg_catalog.pg_class pc, pg_catalog.pg_inherits pi, pg_catalog.pg_namespace pn
			WHERE
				pc.oid=pi.inhrelid
				AND pc.relnamespace=pn.oid
				AND pi.inhparent = (SELECT oid from pg_catalog.pg_class WHERE relname='{$table}'
					AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = '{$c_schema}'))
		";

        return $this->selectSet($sql);
    }

    /**
     * Returns the SQL definition for the table.
     *
     * @pre MUST be run within a transaction
     * @param           $table The table to define
     * @param bool|True $clean True to issue drop command, false otherwise
     * @return \PHPPgAdmin\Database\A string containing the formatted SQL code
     */
    public function getTableDefPrefix($table, $clean = false)
    {
        // Fetch table
        $t = $this->getTable($table);
        if (!is_object($t) || $t->recordCount() != 1) {
            $this->rollbackTransaction();

            return null;
        }
        $this->fieldClean($t->fields['relname']);
        $this->fieldClean($t->fields['nspname']);

        // Fetch attributes
        $atts = $this->getTableAttributes($table);
        if (!is_object($atts)) {
            $this->rollbackTransaction();

            return null;
        }

        // Fetch constraints
        $cons = $this->getConstraints($table);
        if (!is_object($cons)) {
            $this->rollbackTransaction();

            return null;
        }

        // Output a reconnect command to create the table as the correct user
        $sql = $this->getChangeUserSQL($t->fields['relowner']) . "\n\n";

        // Set schema search path
        $sql .= "SET search_path = \"{$t->fields['nspname']}\", pg_catalog;\n\n";

        // Begin CREATE TABLE definition
        $sql .= "-- Definition\n\n";
        // DROP TABLE must be fully qualified in case a table with the same name exists
        // in pg_catalog.
        if (!$clean) {
            $sql .= '-- ';
        }

        $sql .= 'DROP TABLE ';
        $sql .= "\"{$t->fields['nspname']}\".\"{$t->fields['relname']}\";\n";
        $sql .= "CREATE TABLE \"{$t->fields['nspname']}\".\"{$t->fields['relname']}\" (\n";

        // Output all table columns
        $col_comments_sql = ''; // Accumulate comments on columns
        $num              = $atts->recordCount() + $cons->recordCount();
        $i                = 1;
        while (!$atts->EOF) {
            $this->fieldClean($atts->fields['attname']);
            $sql .= "    \"{$atts->fields['attname']}\"";
            // Dump SERIAL and BIGSERIAL columns correctly
            if ($this->phpBool($atts->fields['attisserial']) &&
                ($atts->fields['type'] == 'integer' || $atts->fields['type'] == 'bigint')) {
                if ($atts->fields['type'] == 'integer') {
                    $sql .= ' SERIAL';
                } else {
                    $sql .= ' BIGSERIAL';
                }
            } else {
                $sql .= ' ' . $this->formatType($atts->fields['type'], $atts->fields['atttypmod']);

                // Add NOT NULL if necessary
                if ($this->phpBool($atts->fields['attnotnull'])) {
                    $sql .= ' NOT NULL';
                }

                // Add default if necessary
                if ($atts->fields['adsrc'] !== null) {
                    $sql .= " DEFAULT {$atts->fields['adsrc']}";
                }
            }

            // Output comma or not
            if ($i < $num) {
                $sql .= ",\n";
            } else {
                $sql .= "\n";
            }

            // Does this column have a comment?
            if ($atts->fields['comment'] !== null) {
                $this->clean($atts->fields['comment']);
                $col_comments_sql .= "COMMENT ON COLUMN \"{$t->fields['relname']}\".\"{$atts->fields['attname']}\"  IS '{$atts->fields['comment']}';\n";
            }

            $atts->moveNext();
            $i++;
        }
        // Output all table constraints
        while (!$cons->EOF) {
            $this->fieldClean($cons->fields['conname']);
            $sql .= "    CONSTRAINT \"{$cons->fields['conname']}\" ";
            // Nasty hack to support pre-7.4 PostgreSQL
            if ($cons->fields['consrc'] !== null) {
                $sql .= $cons->fields['consrc'];
            } else {
                switch ($cons->fields['contype']) {
                    case 'p':
                        $keys = $this->getAttributeNames($table, explode(' ', $cons->fields['indkey']));
                        $sql .= 'PRIMARY KEY (' . join(',', $keys) . ')';
                        break;
                    case 'u':
                        $keys = $this->getAttributeNames($table, explode(' ', $cons->fields['indkey']));
                        $sql .= 'UNIQUE (' . join(',', $keys) . ')';
                        break;
                    default:
                        // Unrecognised constraint
                        $this->rollbackTransaction();

                        return null;
                }
            }

            // Output comma or not
            if ($i < $num) {
                $sql .= ",\n";
            } else {
                $sql .= "\n";
            }

            $cons->moveNext();
            $i++;
        }

        $sql .= ')';

        // @@@@ DUMP CLUSTERING INFORMATION

        // Inherits
        /*
         * XXX: This is currently commented out as handling inheritance isn't this simple.
         * You also need to make sure you don't dump inherited columns and defaults, as well
         * as inherited NOT NULL and CHECK constraints.  So for the time being, we just do
         * not claim to support inheritance.
        $parents = $this->getTableParents($table);
        if ($parents->recordCount() > 0) {
        $sql .= " INHERITS (";
        while (!$parents->EOF) {
        $this->fieldClean($parents->fields['relname']);
        // Qualify the parent table if it's in another schema
        if ($parents->fields['schemaname'] != $this->_schema) {
        $this->fieldClean($parents->fields['schemaname']);
        $sql .= "\"{$parents->fields['schemaname']}\".";
        }
        $sql .= "\"{$parents->fields['relname']}\"";

        $parents->moveNext();
        if (!$parents->EOF) $sql .= ', ';
        }
        $sql .= ")";
        }
         */

        // Handle WITHOUT OIDS
        if ($this->hasObjectID($table)) {
            $sql .= ' WITH OIDS';
        } else {
            $sql .= ' WITHOUT OIDS';
        }

        $sql .= ";\n";

        // Column storage and statistics
        $atts->moveFirst();
        $first = true;
        while (!$atts->EOF) {
            $this->fieldClean($atts->fields['attname']);
            // Statistics first
            if ($atts->fields['attstattarget'] >= 0) {
                if ($first) {
                    $sql .= "\n";
                    $first = false;
                }
                $sql .= "ALTER TABLE ONLY \"{$t->fields['nspname']}\".\"{$t->fields['relname']}\" ALTER COLUMN \"{$atts->fields['attname']}\" SET STATISTICS {$atts->fields['attstattarget']};\n";
            }
            // Then storage
            if ($atts->fields['attstorage'] != $atts->fields['typstorage']) {
                switch ($atts->fields['attstorage']) {
                    case 'p':
                        $storage = 'PLAIN';
                        break;
                    case 'e':
                        $storage = 'EXTERNAL';
                        break;
                    case 'm':
                        $storage = 'MAIN';
                        break;
                    case 'x':
                        $storage = 'EXTENDED';
                        break;
                    default:
                        // Unknown storage type
                        $this->rollbackTransaction();

                        return null;
                }
                $sql .= "ALTER TABLE ONLY \"{$t->fields['nspname']}\".\"{$t->fields['relname']}\" ALTER COLUMN \"{$atts->fields['attname']}\" SET STORAGE {$storage};\n";
            }

            $atts->moveNext();
        }

        // Comment
        if ($t->fields['relcomment'] !== null) {
            $this->clean($t->fields['relcomment']);
            $sql .= "\n-- Comment\n\n";
            $sql .= "COMMENT ON TABLE \"{$t->fields['nspname']}\".\"{$t->fields['relname']}\" IS '{$t->fields['relcomment']}';\n";
        }

        // Add comments on columns, if any
        if ($col_comments_sql != '') {
            $sql .= $col_comments_sql;
        }

        // Privileges
        $privs = $this->getPrivileges($table, 'table');
        if (!is_array($privs)) {
            $this->rollbackTransaction();

            return null;
        }

        if (sizeof($privs) > 0) {
            $sql .= "\n-- Privileges\n\n";
            /*
             * Always start with REVOKE ALL FROM PUBLIC, so that we don't have to
             * wire-in knowledge about the default public privileges for different
             * kinds of objects.
             */
            $sql .= "REVOKE ALL ON TABLE \"{$t->fields['nspname']}\".\"{$t->fields['relname']}\" FROM PUBLIC;\n";
            foreach ($privs as $v) {
                // Get non-GRANT OPTION privs
                $nongrant = array_diff($v[2], $v[4]);

                // Skip empty or owner ACEs
                if (sizeof($v[2]) == 0 || ($v[0] == 'user' && $v[1] == $t->fields['relowner'])) {
                    continue;
                }

                // Change user if necessary
                if ($this->hasGrantOption() && $v[3] != $t->fields['relowner']) {
                    $grantor = $v[3];
                    $this->clean($grantor);
                    $sql .= "SET SESSION AUTHORIZATION '{$grantor}';\n";
                }

                // Output privileges with no GRANT OPTION
                $sql .= 'GRANT ' . join(', ', $nongrant) . " ON TABLE \"{$t->fields['relname']}\" TO ";
                switch ($v[0]) {
                    case 'public':
                        $sql .= "PUBLIC;\n";
                        break;
                    case 'user':
                        $this->fieldClean($v[1]);
                        $sql .= "\"{$v[1]}\";\n";
                        break;
                    case 'group':
                        $this->fieldClean($v[1]);
                        $sql .= "GROUP \"{$v[1]}\";\n";
                        break;
                    default:
                        // Unknown privilege type - fail
                        $this->rollbackTransaction();

                        return null;
                }

                // Reset user if necessary
                if ($this->hasGrantOption() && $v[3] != $t->fields['relowner']) {
                    $sql .= "RESET SESSION AUTHORIZATION;\n";
                }

                // Output privileges with GRANT OPTION

                // Skip empty or owner ACEs
                if (!$this->hasGrantOption() || sizeof($v[4]) == 0) {
                    continue;
                }

                // Change user if necessary
                if ($this->hasGrantOption() && $v[3] != $t->fields['relowner']) {
                    $grantor = $v[3];
                    $this->clean($grantor);
                    $sql .= "SET SESSION AUTHORIZATION '{$grantor}';\n";
                }

                $sql .= 'GRANT ' . join(', ', $v[4]) . " ON \"{$t->fields['relname']}\" TO ";
                switch ($v[0]) {
                    case 'public':
                        $sql .= 'PUBLIC';
                        break;
                    case 'user':
                        $this->fieldClean($v[1]);
                        $sql .= "\"{$v[1]}\"";
                        break;
                    case 'group':
                        $this->fieldClean($v[1]);
                        $sql .= "GROUP \"{$v[1]}\"";
                        break;
                    default:
                        // Unknown privilege type - fail
                        return null;
                }
                $sql .= " WITH GRANT OPTION;\n";

                // Reset user if necessary
                if ($this->hasGrantOption() && $v[3] != $t->fields['relowner']) {
                    $sql .= "RESET SESSION AUTHORIZATION;\n";
                }
            }
        }

        // Add a newline to separate data that follows (if any)
        $sql .= "\n";

        return $sql;
    }

    /**
     * Returns table information
     *
     * @param $table The name of the table
     * @return A recordset
     */
    public function getTable($table)
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $this->clean($table);

        $sql = "
			SELECT
			  c.relname, n.nspname, u.usename AS relowner,
			  pg_catalog.obj_description(c.oid, 'pg_class') AS relcomment,
			  (SELECT spcname FROM pg_catalog.pg_tablespace pt WHERE pt.oid=c.reltablespace) AS tablespace
			FROM pg_catalog.pg_class c
			     LEFT JOIN pg_catalog.pg_user u ON u.usesysid = c.relowner
			     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
			WHERE c.relkind = 'r'
			      AND n.nspname = '{$c_schema}'
			      AND n.oid = c.relnamespace
			      AND c.relname = '{$table}'";

        return $this->selectSet($sql);
    }

    /**
     * Retrieve the attribute definition of a table
     *
     * @param $table The name of the table
     * @param $field (optional) The name of a field to return
     * @return All attributes in order
     */
    public function getTableAttributes($table, $field = '')
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $this->clean($table);
        $this->clean($field);

        if ($field == '') {
            // This query is made much more complex by the addition of the 'attisserial' field.
            // The subquery to get that field checks to see if there is an internally dependent
            // sequence on the field.
            $sql = "
				SELECT
					a.attname, a.attnum,
					pg_catalog.format_type(a.atttypid, a.atttypmod) as type,
					a.atttypmod,
					a.attnotnull, a.atthasdef, pg_catalog.pg_get_expr(adef.adbin, adef.adrelid, true) as adsrc,
					a.attstattarget, a.attstorage, t.typstorage,
					(
						SELECT 1 FROM pg_catalog.pg_depend pd, pg_catalog.pg_class pc
						WHERE pd.objid=pc.oid
						AND pd.classid=pc.tableoid
						AND pd.refclassid=pc.tableoid
						AND pd.refobjid=a.attrelid
						AND pd.refobjsubid=a.attnum
						AND pd.deptype='i'
						AND pc.relkind='S'
					) IS NOT NULL AS attisserial,
					pg_catalog.col_description(a.attrelid, a.attnum) AS comment
				FROM
					pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_attrdef adef
					ON a.attrelid=adef.adrelid
					AND a.attnum=adef.adnum
					LEFT JOIN pg_catalog.pg_type t ON a.atttypid=t.oid
				WHERE
					a.attrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname='{$table}'
						AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE
						nspname = '{$c_schema}'))
					AND a.attnum > 0 AND NOT a.attisdropped
				ORDER BY a.attnum";
        } else {
            $sql = "
				SELECT
					a.attname, a.attnum,
					pg_catalog.format_type(a.atttypid, a.atttypmod) as type,
					pg_catalog.format_type(a.atttypid, NULL) as base_type,
					a.atttypmod,
					a.attnotnull, a.atthasdef, pg_catalog.pg_get_expr(adef.adbin, adef.adrelid, true) as adsrc,
					a.attstattarget, a.attstorage, t.typstorage,
					pg_catalog.col_description(a.attrelid, a.attnum) AS comment
				FROM
					pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_attrdef adef
					ON a.attrelid=adef.adrelid
					AND a.attnum=adef.adnum
					LEFT JOIN pg_catalog.pg_type t ON a.atttypid=t.oid
				WHERE
					a.attrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname='{$table}'
						AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE
						nspname = '{$c_schema}'))
					AND a.attname = '{$field}'";
        }

        return $this->selectSet($sql);
    }

    /**
     * Returns a list of all constraints on a table
     *
     * @param $table The table to find rules for
     * @return A recordset
     */
    public function getConstraints($table)
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $this->clean($table);

        // This SQL is greatly complicated by the need to retrieve
        // index clustering information for primary and unique constraints
        $sql = "SELECT
				pc.conname,
				pg_catalog.pg_get_constraintdef(pc.oid, true) AS consrc,
				pc.contype,
				CASE WHEN pc.contype='u' OR pc.contype='p' THEN (
					SELECT
						indisclustered
					FROM
						pg_catalog.pg_depend pd,
						pg_catalog.pg_class pl,
						pg_catalog.pg_index pi
					WHERE
						pd.refclassid=pc.tableoid
						AND pd.refobjid=pc.oid
						AND pd.objid=pl.oid
						AND pl.oid=pi.indexrelid
				) ELSE
					NULL
				END AS indisclustered
			FROM
				pg_catalog.pg_constraint pc
			WHERE
				pc.conrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname='{$table}'
					AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace
					WHERE nspname='{$c_schema}'))
			ORDER BY
				1
		";

        return $this->selectSet($sql);
    }

    /**
     * Returns the SQL for changing the current user
     *
     * @param $user The user to change to
     * @return The SQL
     */
    public function getChangeUserSQL($user)
    {
        $this->clean($user);

        return "SET SESSION AUTHORIZATION '{$user}';";
    }

    /**
     * Change a parameter from 't' or 'f' to a boolean, (others evaluate to false)
     *
     * @param $parameter the parameter
     * @return bool|\PHPPgAdmin\Database\the
     */
    public function phpBool($parameter)
    {
        $parameter = ($parameter == 't');

        return $parameter;
    }

    /**
     * Formats a type correctly for display.  Postgres 7.0 had no 'format_type'
     * built-in function, and hence we need to do it manually.
     *
     * @param $typname The name of the type
     * @param $typmod  The contents of the typmod field
     * @return bool|\PHPPgAdmin\Database\The|string
     */
    public function formatType($typname, $typmod)
    {
        // This is a specific constant in the 7.0 source
        $varhdrsz = 4;

        // If the first character is an underscore, it's an array type
        $is_array = false;
        if (substr($typname, 0, 1) == '_') {
            $is_array = true;
            $typname  = substr($typname, 1);
        }

        // Show lengths on bpchar and varchar
        if ($typname == 'bpchar') {
            $len  = $typmod - $varhdrsz;
            $temp = 'character';
            if ($len > 1) {
                $temp .= "({$len})";
            }
        } elseif ($typname == 'varchar') {
            $temp = 'character varying';
            if ($typmod != -1) {
                $temp .= '(' . ($typmod - $varhdrsz) . ')';
            }
        } elseif ($typname == 'numeric') {
            $temp = 'numeric';
            if ($typmod != -1) {
                $tmp_typmod = $typmod - $varhdrsz;
                $precision  = ($tmp_typmod >> 16) & 0xffff;
                $scale      = $tmp_typmod & 0xffff;
                $temp .= "({$precision}, {$scale})";
            }
        } else {
            $temp = $typname;
        }

        // Add array qualifier if it's an array
        if ($is_array) {
            $temp .= '[]';
        }

        return $temp;
    }

    /**
     * Given an array of attnums and a relation, returns an array mapping
     * attribute number to attribute name.
     *
     * @param $table The table to get attributes for
     * @param $atts  An array of attribute numbers
     * @return An array mapping attnum to attname
     * @return -1 $atts must be an array
     * @return -2 wrong number of attributes found
     */
    public function getAttributeNames($table, $atts)
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $this->clean($table);
        $this->arrayClean($atts);

        if (!is_array($atts)) {
            return -1;
        }

        if (sizeof($atts) == 0) {
            return [];
        }

        $sql = "SELECT attnum, attname FROM pg_catalog.pg_attribute WHERE
			attrelid=(SELECT oid FROM pg_catalog.pg_class WHERE relname='{$table}' AND
			relnamespace=(SELECT oid FROM pg_catalog.pg_namespace WHERE nspname='{$c_schema}'))
			AND attnum IN ('" . join("','", $atts) . "')";

        $rs = $this->selectSet($sql);
        if ($rs->recordCount() != sizeof($atts)) {
            return -2;
        }

        $temp = [];
        while (!$rs->EOF) {
            $temp[$rs->fields['attnum']] = $rs->fields['attname'];
            $rs->moveNext();
        }

        return $temp;
    }

    /**
     * Cleans (escapes) an array
     *
     * @param $arr The array to clean, by reference
     * @return The cleaned array
     */
    public function arrayClean(&$arr)
    {
        foreach ($arr as $k => $v) {
            if ($v === null) {
                continue;
            }

            $arr[$k] = pg_escape_string($v);
        }

        return $arr;
    }

    /**
     * Checks to see whether or not a table has a unique id column
     *
     * @param $table The table name
     * @return True if it has a unique id, false otherwise
     * @return null error
     **/
    public function hasObjectID($table)
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $this->clean($table);

        $sql = "SELECT relhasoids FROM pg_catalog.pg_class WHERE relname='{$table}'
			AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname='{$c_schema}')";

        $rs = $this->selectSet($sql);
        if ($rs->recordCount() != 1) {
            return null;
        }

        $rs->fields['relhasoids'] = $this->phpBool($rs->fields['relhasoids']);

        return $rs->fields['relhasoids'];
    }

    /**
     * Grabs an array of users and their privileges for an object,
     * given its type.
     *
     * @param $object The name of the object whose privileges are to be retrieved
     * @param $type   The type of the object (eg. database, schema, relation, function or language)
     * @param $table  Optional, column's table if type = column
     * @return Privileges array
     * @return -1 invalid type
     * @return -2 object not found
     * @return -3 unknown privilege type
     */
    public function getPrivileges($object, $type, $table = null)
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $this->clean($object);

        switch ($type) {
            case 'column':
                $this->clean($table);
                $sql = "
					SELECT E'{' || pg_catalog.array_to_string(attacl, E',') || E'}' as acl
					FROM pg_catalog.pg_attribute a
						LEFT JOIN pg_catalog.pg_class c ON (a.attrelid = c.oid)
						LEFT JOIN pg_catalog.pg_namespace n ON (c.relnamespace=n.oid)
					WHERE n.nspname='{$c_schema}'
						AND c.relname='{$table}'
						AND a.attname='{$object}'";
                break;
            case 'table':
            case 'view':
            case 'sequence':
                $sql = "
					SELECT relacl AS acl FROM pg_catalog.pg_class
					WHERE relname='{$object}'
						AND relnamespace=(SELECT oid FROM pg_catalog.pg_namespace
							WHERE nspname='{$c_schema}')";
                break;
            case 'database':
                $sql = "SELECT datacl AS acl FROM pg_catalog.pg_database WHERE datname='{$object}'";
                break;
            case 'function':
                // Since we fetch functions by oid, they are already constrained to
                // the current schema.
                $sql = "SELECT proacl AS acl FROM pg_catalog.pg_proc WHERE oid='{$object}'";
                break;
            case 'language':
                $sql = "SELECT lanacl AS acl FROM pg_catalog.pg_language WHERE lanname='{$object}'";
                break;
            case 'schema':
                $sql = "SELECT nspacl AS acl FROM pg_catalog.pg_namespace WHERE nspname='{$object}'";
                break;
            case 'tablespace':
                $sql = "SELECT spcacl AS acl FROM pg_catalog.pg_tablespace WHERE spcname='{$object}'";
                break;
            default:
                return -1;
        }

        // Fetch the ACL for object
        $acl = $this->selectField($sql, 'acl');
        if ($acl == -1) {
            return -2;
        }

        if ($acl == '' || $acl == null) {
            return [];
        } else {
            return $this->_parseACL($acl);
        }
    }

    /**
     * Internal function used for parsing ACLs
     *
     * @param $acl The ACL to parse (of type aclitem[])
     * @return Privileges array
     */
    public function _parseACL($acl)
    {
        // Take off the first and last characters (the braces)
        $acl = substr($acl, 1, strlen($acl) - 2);

        // Pick out individual ACE's by carefully parsing.  This is necessary in order
        // to cope with usernames and stuff that contain commas
        $aces      = [];
        $i         = $j         = 0;
        $in_quotes = false;
        while ($i < strlen($acl)) {
            // If current char is a double quote and it's not escaped, then
            // enter quoted bit
            $char = substr($acl, $i, 1);
            if ($char == '"' && ($i == 0 || substr($acl, $i - 1, 1) != '\\')) {
                $in_quotes = !$in_quotes;
            } elseif ($char == ',' && !$in_quotes) {
                // Add text so far to the array
                $aces[] = substr($acl, $j, $i - $j);
                $j      = $i + 1;
            }
            $i++;
        }
        // Add final text to the array
        $aces[] = substr($acl, $j);

        // Create the array to be returned
        $temp = [];

        // For each ACE, generate an entry in $temp
        foreach ($aces as $v) {

            // If the ACE begins with a double quote, strip them off both ends
            // and unescape backslashes and double quotes
            $unquote = false;
            if (strpos($v, '"') === 0) {
                $v = substr($v, 1, strlen($v) - 2);
                $v = str_replace('\\"', '"', $v);
                $v = str_replace('\\\\', '\\', $v);
            }

            // Figure out type of ACE (public, user or group)
            if (strpos($v, '=') === 0) {
                $atype = 'public';
            } else {
                if ($this->hasRoles()) {
                    $atype = 'role';
                } else {
                    if (strpos($v, 'group ') === 0) {
                        $atype = 'group';
                        // Tear off 'group' prefix
                        $v = substr($v, 6);
                    } else {
                        $atype = 'user';
                    }
                }
            }

            // Break on unquoted equals sign...
            $i         = 0;
            $in_quotes = false;
            $entity    = null;
            $chars     = null;
            while ($i < strlen($v)) {
                // If current char is a double quote and it's not escaped, then
                // enter quoted bit
                $char      = substr($v, $i, 1);
                $next_char = substr($v, $i + 1, 1);
                if ($char == '"' && ($i == 0 || $next_char != '"')) {
                    $in_quotes = !$in_quotes;
                } // Skip over escaped double quotes
                elseif ($char == '"' && $next_char == '"') {
                    $i++;
                } elseif ($char == '=' && !$in_quotes) {
                    // Split on current equals sign
                    $entity = substr($v, 0, $i);
                    $chars  = substr($v, $i + 1);
                    break;
                }
                $i++;
            }

            // Check for quoting on entity name, and unescape if necessary
            if (strpos($entity, '"') === 0) {
                $entity = substr($entity, 1, strlen($entity) - 2);
                $entity = str_replace('""', '"', $entity);
            }

            // New row to be added to $temp
            // (type, grantee, privileges, grantor, grant option?
            $row = [$atype, $entity, [], '', []];

            // Loop over chars and add privs to $row
            for ($i = 0; $i < strlen($chars); $i++) {
                // Append to row's privs list the string representing
                // the privilege
                $char = substr($chars, $i, 1);
                if ($char == '*') {
                    $row[4][] = $this->privmap[substr($chars, $i - 1, 1)];
                } elseif ($char == '/') {
                    $grantor = substr($chars, $i + 1);
                    // Check for quoting
                    if (strpos($grantor, '"') === 0) {
                        $grantor = substr($grantor, 1, strlen($grantor) - 2);
                        $grantor = str_replace('""', '"', $grantor);
                    }
                    $row[3] = $grantor;
                    break;
                } else {
                    if (!isset($this->privmap[$char])) {
                        return -3;
                    }

                    $row[2][] = $this->privmap[$char];
                }
            }

            // Append row to temp
            $temp[] = $row;
        }

        return $temp;
    }

    public function hasRoles()
    {
        return true;
    }

    public function hasGrantOption()
    {
        return true;
    }

    /**
     * Returns extra table definition information that is most usefully
     * dumped after the table contents for speed and efficiency reasons
     *
     * @param $table The table to define
     * @return A string containing the formatted SQL code
     * @return null On error
     */
    public function getTableDefSuffix($table)
    {
        $sql = '';

        // Indexes
        $indexes = $this->getIndexes($table);
        if (!is_object($indexes)) {
            $this->rollbackTransaction();

            return null;
        }

        if ($indexes->recordCount() > 0) {
            $sql .= "\n-- Indexes\n\n";
            while (!$indexes->EOF) {
                $sql .= $indexes->fields['inddef'] . ";\n";

                $indexes->moveNext();
            }
        }

        // Triggers
        $triggers = $this->getTriggers($table);
        if (!is_object($triggers)) {
            $this->rollbackTransaction();

            return null;
        }

        if ($triggers->recordCount() > 0) {
            $sql .= "\n-- Triggers\n\n";
            while (!$triggers->EOF) {

                $sql .= $triggers->fields['tgdef'];
                $sql .= ";\n";

                $triggers->moveNext();
            }
        }

        // Rules
        $rules = $this->getRules($table);
        if (!is_object($rules)) {
            $this->rollbackTransaction();

            return null;
        }

        if ($rules->recordCount() > 0) {
            $sql .= "\n-- Rules\n\n";
            while (!$rules->EOF) {
                $sql .= $rules->fields['definition'] . "\n";

                $rules->moveNext();
            }
        }

        return $sql;
    }

    /**
     * Grabs a list of indexes for a table
     *
     * @param \PHPPgAdmin\Database\The|string $table  The name of a table whose indexes to retrieve
     * @param bool|\PHPPgAdmin\Database\Only  $unique Only get unique/pk indexes
     * @return \PHPPgAdmin\Database\A recordset
     */
    public function getIndexes($table = '', $unique = false)
    {
        $this->clean($table);

        $sql = "
			SELECT c2.relname AS indname, i.indisprimary, i.indisunique, i.indisclustered,
				pg_catalog.pg_get_indexdef(i.indexrelid, 0, true) AS inddef
			FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i
			WHERE c.relname = '{$table}' AND pg_catalog.pg_table_is_visible(c.oid)
				AND c.oid = i.indrelid AND i.indexrelid = c2.oid
		";
        if ($unique) {
            $sql .= ' AND i.indisunique ';
        }

        $sql .= ' ORDER BY c2.relname';

        return $this->selectSet($sql);
    }

    /**
     * Grabs a list of triggers on a table
     *
     * @param \PHPPgAdmin\Database\The|string $table The name of a table whose triggers to retrieve
     * @return \PHPPgAdmin\Database\A recordset
     */
    public function getTriggers($table = '')
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $this->clean($table);

        $sql = "SELECT
				t.tgname, pg_catalog.pg_get_triggerdef(t.oid) AS tgdef,
				CASE WHEN t.tgenabled = 'D' THEN FALSE ELSE TRUE END AS tgenabled, p.oid AS prooid,
				p.proname || ' (' || pg_catalog.oidvectortypes(p.proargtypes) || ')' AS proproto,
				ns.nspname AS pronamespace
			FROM pg_catalog.pg_trigger t, pg_catalog.pg_proc p, pg_catalog.pg_namespace ns
			WHERE t.tgrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname='{$table}'
				AND relnamespace=(SELECT oid FROM pg_catalog.pg_namespace WHERE nspname='{$c_schema}'))
				AND ( tgconstraint = 0 OR NOT EXISTS
						(SELECT 1 FROM pg_catalog.pg_depend d    JOIN pg_catalog.pg_constraint c
							ON (d.refclassid = c.tableoid AND d.refobjid = c.oid)
						WHERE d.classid = t.tableoid AND d.objid = t.oid AND d.deptype = 'i' AND c.contype = 'f'))
				AND p.oid=t.tgfoid
				AND p.pronamespace = ns.oid";

        return $this->selectSet($sql);
    }

    /**
     * Returns a list of all rules on a table OR view
     *
     * @param $table The table to find rules for
     * @return A recordset
     */
    public function getRules($table)
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $this->clean($table);

        $sql = "
			SELECT *
			FROM pg_catalog.pg_rules
			WHERE
				schemaname='{$c_schema}' AND tablename='{$table}'
			ORDER BY rulename
		";

        return $this->selectSet($sql);
    }

    /**
     * Creates a new table in the database
     *
     * @param $name        The name of the table
     * @param $fields      The number of fields
     * @param $field       An array of field names
     * @param $type        An array of field types
     * @param $array       An array of '' or '[]' for each type if it's an array or not
     * @param $length      An array of field lengths
     * @param $notnull     An array of not null
     * @param $default     An array of default values
     * @param $withoutoids True if WITHOUT OIDS, false otherwise
     * @param $colcomment  An array of comments
     * @param $tblcomment
     * @param $tablespace  The tablespace name ('' means none/default)
     * @param $uniquekey   An Array indicating the fields that are unique (those indexes that are set)
     * @param $primarykey  An Array indicating the field used for the primarykey (those indexes that are set)
     * @return bool|int 0 success
     * @internal param \PHPPgAdmin\Database\Table $comment comment
     */
    public function createTable(
        $name,
        $fields,
        $field,
        $type,
        $array,
        $length,
        $notnull,
        $default,
        $withoutoids,
        $colcomment,
        $tblcomment,
        $tablespace,
        $uniquekey,
        $primarykey
    ) {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($name);

        $status = $this->beginTransaction();
        if ($status != 0) {
            return -1;
        }

        $found       = false;
        $first       = true;
        $comment_sql = ''; //Accumulate comments for the columns
        $sql         = "CREATE TABLE \"{$f_schema}\".\"{$name}\" (";
        for ($i = 0; $i < $fields; $i++) {
            $this->fieldClean($field[$i]);
            $this->clean($type[$i]);
            $this->clean($length[$i]);
            $this->clean($colcomment[$i]);

            // Skip blank columns - for user convenience
            if ($field[$i] == '' || $type[$i] == '') {
                continue;
            }

            // If not the first column, add a comma
            if (!$first) {
                $sql .= ', ';
            } else {
                $first = false;
            }

            switch ($type[$i]) {
                // Have to account for weird placing of length for with/without
                // time zone types
                case 'timestamp with time zone':
                case 'timestamp without time zone':
                    $qual = substr($type[$i], 9);
                    $sql .= "\"{$field[$i]}\" timestamp";
                    if ($length[$i] != '') {
                        $sql .= "({$length[$i]})";
                    }

                    $sql .= $qual;
                    break;
                case 'time with time zone':
                case 'time without time zone':
                    $qual = substr($type[$i], 4);
                    $sql .= "\"{$field[$i]}\" time";
                    if ($length[$i] != '') {
                        $sql .= "({$length[$i]})";
                    }

                    $sql .= $qual;
                    break;
                default:
                    $sql .= "\"{$field[$i]}\" {$type[$i]}";
                    if ($length[$i] != '') {
                        $sql .= "({$length[$i]})";
                    }
            }
            // Add array qualifier if necessary
            if ($array[$i] == '[]') {
                $sql .= '[]';
            }

            // Add other qualifiers
            if (!isset($primarykey[$i])) {
                if (isset($uniquekey[$i])) {
                    $sql .= ' UNIQUE';
                }

                if (isset($notnull[$i])) {
                    $sql .= ' NOT NULL';
                }
            }
            if ($default[$i] != '') {
                $sql .= " DEFAULT {$default[$i]}";
            }

            if ($colcomment[$i] != '') {
                $comment_sql .= "COMMENT ON COLUMN \"{$name}\".\"{$field[$i]}\" IS '{$colcomment[$i]}';\n";
            }

            $found = true;
        }

        if (!$found) {
            return -1;
        }

        // PRIMARY KEY
        $primarykeycolumns = [];
        for ($i = 0; $i < $fields; $i++) {
            if (isset($primarykey[$i])) {
                $primarykeycolumns[] = "\"{$field[$i]}\"";
            }
        }
        if (count($primarykeycolumns) > 0) {
            $sql .= ', PRIMARY KEY (' . implode(', ', $primarykeycolumns) . ')';
        }

        $sql .= ')';

        // WITHOUT OIDS
        if ($withoutoids) {
            $sql .= ' WITHOUT OIDS';
        } else {
            $sql .= ' WITH OIDS';
        }

        // Tablespace
        if ($this->hasTablespaces() && $tablespace != '') {
            $this->fieldClean($tablespace);
            $sql .= " TABLESPACE \"{$tablespace}\"";
        }

        $status = $this->execute($sql);
        if ($status) {
            $this->rollbackTransaction();

            return -1;
        }

        if ($tblcomment != '') {
            $status = $this->setComment('TABLE', '', $name, $tblcomment, true);
            if ($status) {
                $this->rollbackTransaction();

                return -1;
            }
        }

        if ($comment_sql != '') {
            $status = $this->execute($comment_sql);
            if ($status) {
                $this->rollbackTransaction();

                return -1;
            }
        }

        return $this->endTransaction();
    }

    /**
     * Creates a new table in the database copying attribs and other properties from another table
     *
     * @param                                 $name        The name of the table
     * @param                                 $like        an array giving the schema ans the name of the table from which attribs are copying
     *                                                     from: array(
     *                                                     'table' => table name,
     *                                                     'schema' => the schema name,
     *                                                     )
     * @param bool|\PHPPgAdmin\Database\if    $defaults    if true, copy the defaults values as well
     * @param bool|\PHPPgAdmin\Database\if    $constraints if true, copy the constraints as well (CHECK on table & attr)
     * @param bool                            $idx
     * @param \PHPPgAdmin\Database\The|string $tablespace  The tablespace name ('' means none/default)
     * @return bool|int
     */
    public function createTableLike($name, $like, $defaults = false, $constraints = false, $idx = false, $tablespace = '')
    {

        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($name);
        $this->fieldClean($like['schema']);
        $this->fieldClean($like['table']);
        $like = "\"{$like['schema']}\".\"{$like['table']}\"";

        $status = $this->beginTransaction();
        if ($status != 0) {
            return -1;
        }

        $sql = "CREATE TABLE \"{$f_schema}\".\"{$name}\" (LIKE {$like}";

        if ($defaults) {
            $sql .= ' INCLUDING DEFAULTS';
        }

        if ($this->hasCreateTableLikeWithConstraints() && $constraints) {
            $sql .= ' INCLUDING CONSTRAINTS';
        }

        if ($this->hasCreateTableLikeWithIndexes() && $idx) {
            $sql .= ' INCLUDING INDEXES';
        }

        $sql .= ')';

        if ($this->hasTablespaces() && $tablespace != '') {
            $this->fieldClean($tablespace);
            $sql .= " TABLESPACE \"{$tablespace}\"";
        }

        $status = $this->execute($sql);
        if ($status) {
            $this->rollbackTransaction();

            return -1;
        }

        return $this->endTransaction();
    }

    public function hasCreateTableLikeWithConstraints()
    {
        return true;
    }

    public function hasCreateTableLikeWithIndexes()
    {
        return true;
    }

    /**
     * Alter table properties
     *
     * @param $table      The name of the table
     * @param $name       The new name for the table
     * @param $owner      The new owner for the table
     * @param $schema     The new schema for the table
     * @param $comment    The comment on the table
     * @param $tablespace The new tablespace for the table ('' means leave as is)
     * @return bool|int 0 success
     */
    public function alterTable($table, $name, $owner, $schema, $comment, $tablespace)
    {

        $data = $this->getTable($table);

        if ($data->recordCount() != 1) {
            return -2;
        }

        $status = $this->beginTransaction();
        if ($status != 0) {
            $this->rollbackTransaction();

            return -1;
        }

        $status = $this->_alterTable($data, $name, $owner, $schema, $comment, $tablespace);

        if ($status != 0) {
            $this->rollbackTransaction();

            return $status;
        }

        return $this->endTransaction();
    }

    /**
     * Protected method which alter a table
     * SHOULDN'T BE CALLED OUTSIDE OF A TRANSACTION
     *
     * @param $tblrs      The table recordSet returned by getTable()
     * @param $name       The new name for the table
     * @param $owner      The new owner for the table
     * @param $schema     The new schema for the table
     * @param $comment    The comment on the table
     * @param $tablespace The new tablespace for the table ('' means leave as is)
     * @return int 0 success
     */
    protected function _alterTable($tblrs, $name, $owner, $schema, $comment, $tablespace)
    {

        $this->fieldArrayClean($tblrs->fields);

        // Comment
        $status = $this->setComment('TABLE', '', $tblrs->fields['relname'], $comment);
        if ($status != 0) {
            return -4;
        }

        // Owner
        $this->fieldClean($owner);
        $status = $this->alterTableOwner($tblrs, $owner);
        if ($status != 0) {
            return -5;
        }

        // Tablespace
        $this->fieldClean($tablespace);
        $status = $this->alterTableTablespace($tblrs, $tablespace);
        if ($status != 0) {
            return -6;
        }

        // Rename
        $this->fieldClean($name);
        $status = $this->alterTableName($tblrs, $name);
        if ($status != 0) {
            return -3;
        }

        // Schema
        $this->fieldClean($schema);
        $status = $this->alterTableSchema($tblrs, $schema);
        if ($status != 0) {
            return -7;
        }

        return 0;
    }

    /**
     * Alter a table's owner
     * /!\ this function is called from _alterTable which take care of escaping fields
     *
     * @param      $tblrs The table RecordSet returned by getTable()
     * @param null $owner
     * @return int|\PHPPgAdmin\Database\A 0 success
     * @internal param \PHPPgAdmin\Database\The $name new table's owner
     */
    public function alterTableOwner($tblrs, $owner = null)
    {
        /* vars cleaned in _alterTable */
        if (!empty($owner) && ($tblrs->fields['relowner'] != $owner)) {
            $f_schema = $this->_schema;
            $this->fieldClean($f_schema);
            // If owner has been changed, then do the alteration.  We are
            // careful to avoid this generally as changing owner is a
            // superuser only function.
            $sql = "ALTER TABLE \"{$f_schema}\".\"{$tblrs->fields['relname']}\" OWNER TO \"{$owner}\"";

            return $this->execute($sql);
        }

        return 0;
    }

    /**
     * Alter a table's tablespace
     * /!\ this function is called from _alterTable which take care of escaping fields
     *
     * @param      $tblrs The table RecordSet returned by getTable()
     * @param null $tablespace
     * @return int|\PHPPgAdmin\Database\A 0 success
     * @internal param \PHPPgAdmin\Database\The $name new table's tablespace
     */
    public function alterTableTablespace($tblrs, $tablespace = null)
    {
        /* vars cleaned in _alterTable */
        if (!empty($tablespace) && ($tblrs->fields['tablespace'] != $tablespace)) {
            $f_schema = $this->_schema;
            $this->fieldClean($f_schema);

            // If tablespace has been changed, then do the alteration.  We
            // don't want to do this unnecessarily.
            $sql = "ALTER TABLE \"{$f_schema}\".\"{$tblrs->fields['relname']}\" SET TABLESPACE \"{$tablespace}\"";

            return $this->execute($sql);
        }

        return 0;
    }

    /**
     * Alter a table's name
     * /!\ this function is called from _alterTable which take care of escaping fields
     *
     * @param $tblrs The table RecordSet returned by getTable()
     * @param $name  The new table's name
     * @return int|\PHPPgAdmin\Database\A 0 success
     */
    public function alterTableName($tblrs, $name = null)
    {
        /* vars cleaned in _alterTable */
        // Rename (only if name has changed)
        if (!empty($name) && ($name != $tblrs->fields['relname'])) {
            $f_schema = $this->_schema;
            $this->fieldClean($f_schema);

            $sql    = "ALTER TABLE \"{$f_schema}\".\"{$tblrs->fields['relname']}\" RENAME TO \"{$name}\"";
            $status = $this->execute($sql);
            if ($status == 0) {
                $tblrs->fields['relname'] = $name;
            } else {
                return $status;
            }
        }

        return 0;
    }

    // Row functions

    /**
     * Alter a table's schema
     * /!\ this function is called from _alterTable which take care of escaping fields
     *
     * @param      $tblrs The table RecordSet returned by getTable()
     * @param null $schema
     * @return int|\PHPPgAdmin\Database\A 0 success
     * @internal param \PHPPgAdmin\Database\The $name new table's schema
     */
    public function alterTableSchema($tblrs, $schema = null)
    {
        /* vars cleaned in _alterTable */
        if (!empty($schema) && ($tblrs->fields['nspname'] != $schema)) {
            $f_schema = $this->_schema;
            $this->fieldClean($f_schema);
            // If tablespace has been changed, then do the alteration.  We
            // don't want to do this unnecessarily.
            $sql = "ALTER TABLE \"{$f_schema}\".\"{$tblrs->fields['relname']}\" SET SCHEMA \"{$schema}\"";

            return $this->execute($sql);
        }

        return 0;
    }

    /**
     * Empties a table in the database
     *
     * @param $table The table to be emptied
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function emptyTable($table)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($table);

        $sql = "DELETE FROM \"{$f_schema}\".\"{$table}\"";

        return $this->execute($sql);
    }

    /**
     * Removes a table from the database
     *
     * @param $table   The table to drop
     * @param $cascade True to cascade drop, false to restrict
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function dropTable($table, $cascade)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($table);

        $sql = "DROP TABLE \"{$f_schema}\".\"{$table}\"";
        if ($cascade) {
            $sql .= ' CASCADE';
        }

        return $this->execute($sql);
    }

    /**
     * Add a new column to a table
     *
     * @param $table   The table to add to
     * @param $column  The name of the new column
     * @param $type    The type of the column
     * @param $array   True if array type, false otherwise
     * @param $length  The optional size of the column (ie. 30 for varchar(30))
     * @param $notnull True if NOT NULL, false otherwise
     * @param $default The default for the column.  '' for none.
     * @param $comment
     * @return bool|int 0 success
     */
    public function addColumn($table, $column, $type, $array, $length, $notnull, $default, $comment)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($table);
        $this->fieldClean($column);
        $this->clean($type);
        $this->clean($length);

        if ($length == '') {
            $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" ADD COLUMN \"{$column}\" {$type}";
        } else {
            switch ($type) {
                // Have to account for weird placing of length for with/without
                // time zone types
                case 'timestamp with time zone':
                case 'timestamp without time zone':
                    $qual = substr($type, 9);
                    $sql  = "ALTER TABLE \"{$f_schema}\".\"{$table}\" ADD COLUMN \"{$column}\" timestamp({$length}){$qual}";
                    break;
                case 'time with time zone':
                case 'time without time zone':
                    $qual = substr($type, 4);
                    $sql  = "ALTER TABLE \"{$f_schema}\".\"{$table}\" ADD COLUMN \"{$column}\" time({$length}){$qual}";
                    break;
                default:
                    $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" ADD COLUMN \"{$column}\" {$type}({$length})";
            }
        }

        // Add array qualifier, if requested
        if ($array) {
            $sql .= '[]';
        }

        // If we have advanced column adding, add the extra qualifiers
        if ($this->hasCreateFieldWithConstraints()) {
            // NOT NULL clause
            if ($notnull) {
                $sql .= ' NOT NULL';
            }

            // DEFAULT clause
            if ($default != '') {
                $sql .= ' DEFAULT ' . $default;
            }
        }

        $status = $this->beginTransaction();
        if ($status != 0) {
            return -1;
        }

        $status = $this->execute($sql);
        if ($status != 0) {
            $this->rollbackTransaction();

            return -1;
        }

        $status = $this->setComment('COLUMN', $column, $table, $comment);
        if ($status != 0) {
            $this->rollbackTransaction();

            return -1;
        }

        return $this->endTransaction();
    }

    // Sequence functions

    public function hasCreateFieldWithConstraints()
    {
        return true;
    }

    /**
     * Alters a column in a table
     *
     * @param $table      The table in which the column resides
     * @param $column     The column to alter
     * @param $name       The new name for the column
     * @param $notnull    (boolean) True if not null, false otherwise
     * @param $oldnotnull (boolean) True if column is already not null, false otherwise
     * @param $default    The new default for the column
     * @param $olddefault The old default for the column
     * @param $type       The new type for the column
     * @param $length     The optional size of the column (ie. 30 for varchar(30))
     * @param $array      True if array type, false otherwise
     * @param $oldtype    The old type for the column
     * @param $comment    Comment for the column
     * @return array 0 success
     */
    public function alterColumn(
        $table,
        $column,
        $name,
        $notnull,
        $oldnotnull,
        $default,
        $olddefault,
        $type,
        $length,
        $array,
        $oldtype,
        $comment
    ) {
        // Begin transaction
        $status = $this->beginTransaction();
        $sql    = '';
        if ($status != 0) {
            $this->rollbackTransaction();

            return [-6, $sql];
        }

        // Rename the column, if it has been changed
        if ($column != $name) {
            $status = $this->renameColumn($table, $column, $name);
            if ($status != 0) {
                $this->rollbackTransaction();

                return [-4, $sql];
            }
        }

        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($name);
        $this->fieldClean($table);
        $this->fieldClean($column);

        $toAlter = [];
        // Create the command for changing nullability
        if ($notnull != $oldnotnull) {
            $toAlter[] = "ALTER COLUMN \"{$name}\" " . ($notnull ? 'SET' : 'DROP') . ' NOT NULL';
        }

        // Add default, if it has changed
        if ($default != $olddefault) {
            if ($default == '') {
                $toAlter[] = "ALTER COLUMN \"{$name}\" DROP DEFAULT";
            } else {
                $toAlter[] = "ALTER COLUMN \"{$name}\" SET DEFAULT {$default}";
            }
        }

        // Add type, if it has changed
        if ($length == '') {
            $ftype = $type;
        } else {
            switch ($type) {
                // Have to account for weird placing of length for with/without
                // time zone types
                case 'timestamp with time zone':
                case 'timestamp without time zone':
                    $qual  = substr($type, 9);
                    $ftype = "timestamp({$length}){$qual}";
                    break;
                case 'time with time zone':
                case 'time without time zone':
                    $qual  = substr($type, 4);
                    $ftype = "time({$length}){$qual}";
                    break;
                default:
                    $ftype = "{$type}({$length})";
            }
        }

        // Add array qualifier, if requested
        if ($array) {
            $ftype .= '[]';
        }

        if ($ftype != $oldtype) {
            $toAlter[] = "ALTER COLUMN \"{$name}\" TYPE {$ftype}";
        }

        // Attempt to process the batch alteration, if anything has been changed
        if (!empty($toAlter)) {
            // Initialise an empty SQL string
            $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" "
            . implode(',', $toAlter);

            $status = $this->execute($sql);
            if ($status != 0) {
                $this->rollbackTransaction();

                return [-1, $sql];
            }
        }

        // Update the comment on the column
        $status = $this->setComment('COLUMN', $name, $table, $comment);
        if ($status != 0) {
            $this->rollbackTransaction();

            return [-5, $sql];
        }

        return [$this->endTransaction(), $sql];
    }

    /**
     * Renames a column in a table
     *
     * @param $table   The table containing the column to be renamed
     * @param $column  The column to be renamed
     * @param $newName The new name for the column
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function renameColumn($table, $column, $newName)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($table);
        $this->fieldClean($column);
        $this->fieldClean($newName);

        $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" RENAME COLUMN \"{$column}\" TO \"{$newName}\"";

        return $this->execute($sql);
    }

    /**
     * Sets default value of a column
     *
     * @param $table   The table from which to drop
     * @param $column  The column name to set
     * @param $default The new default value
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function setColumnDefault($table, $column, $default)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($table);
        $this->fieldClean($column);

        $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" ALTER COLUMN \"{$column}\" SET DEFAULT {$default}";

        return $this->execute($sql);
    }

    /**
     * Sets whether or not a column can contain NULLs
     *
     * @param $table  The table that contains the column
     * @param $column The column to alter
     * @param $state  True to set null, false to set not null
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function setColumnNull($table, $column, $state)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($table);
        $this->fieldClean($column);

        $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" ALTER COLUMN \"{$column}\" " . ($state ? 'DROP' : 'SET') . ' NOT NULL';

        return $this->execute($sql);
    }

    /**
     * Drops a column from a table
     *
     * @param $table   The table from which to drop a column
     * @param $column  The column to be dropped
     * @param $cascade True to cascade drop, false to restrict
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function dropColumn($table, $column, $cascade)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($table);
        $this->fieldClean($column);

        $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" DROP COLUMN \"{$column}\"";
        if ($cascade) {
            $sql .= ' CASCADE';
        }

        return $this->execute($sql);
    }

    /**
     * Drops default value of a column
     *
     * @param $table  The table from which to drop
     * @param $column The column name to drop default
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function dropColumnDefault($table, $column)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($table);
        $this->fieldClean($column);

        $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" ALTER COLUMN \"{$column}\" DROP DEFAULT";

        return $this->execute($sql);
    }

    /**
     * Sets up the data object for a dump.  eg. Starts the appropriate
     * transaction, sets variables, etc.
     *
     * @return int 0 success
     */
    public function beginDump()
    {
        // Begin serializable transaction (to dump consistent data)
        $status = $this->beginTransaction();
        if ($status != 0) {
            return -1;
        }

        // Set serializable
        $sql    = 'SET TRANSACTION ISOLATION LEVEL SERIALIZABLE';
        $status = $this->execute($sql);
        if ($status != 0) {
            $this->rollbackTransaction();

            return -1;
        }

        // Set datestyle to ISO
        $sql    = 'SET DATESTYLE = ISO';
        $status = $this->execute($sql);
        if ($status != 0) {
            $this->rollbackTransaction();

            return -1;
        }

        // Set extra_float_digits to 2
        $sql    = 'SET extra_float_digits TO 2';
        $status = $this->execute($sql);
        if ($status != 0) {
            $this->rollbackTransaction();

            return -1;
        }

        return 0;
    }

    /**
     * Ends the data object for a dump.
     *
     * @return bool 0 success
     */
    public function endDump()
    {
        return $this->endTransaction();
    }

    /**
     * Returns a recordset of all columns in a relation.  Used for data export.
     * @@ Note: Really needs to use a cursor
     *
     * @param $relation The name of a relation
     * @param $oids
     * @return \PHPPgAdmin\Database\A recordset on success
     */
    public function dumpRelation($relation, $oids)
    {
        $this->fieldClean($relation);

        // Actually retrieve the rows
        if ($oids) {
            $oid_str = $this->id . ', ';
        } else {
            $oid_str = '';
        }

        return $this->selectSet("SELECT {$oid_str}* FROM \"{$relation}\"");
    }

    /**
     * Returns all available autovacuum per table information.
     *
     * @param \PHPPgAdmin\Database\if|string $table if given, return autovacuum info for the given table or return all informations for all table
     * @return \PHPPgAdmin\Database\A recordset
     */
    public function getTableAutovacuum($table = '')
    {

        $sql = '';

        if ($table !== '') {
            $this->clean($table);
            $c_schema = $this->_schema;
            $this->clean($c_schema);

            $sql = "SELECT c.oid, nspname, relname, pg_catalog.array_to_string(reloptions, E',') AS reloptions
				FROM pg_class c
					LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
				WHERE c.relkind = 'r'::\"char\"
					AND n.nspname NOT IN ('pg_catalog','information_schema')
					AND c.reloptions IS NOT NULL
					AND c.relname = '{$table}' AND n.nspname = '{$c_schema}'
				ORDER BY nspname, relname";
        } else {
            $sql = "SELECT c.oid, nspname, relname, pg_catalog.array_to_string(reloptions, E',') AS reloptions
				FROM pg_class c
					LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
				WHERE c.relkind = 'r'::\"char\"
					AND n.nspname NOT IN ('pg_catalog','information_schema')
					AND c.reloptions IS NOT NULL
				ORDER BY nspname, relname";
        }

        /* tmp var to parse the results */
        $_autovacs = $this->selectSet($sql);

        /* result aray to return as RS */
        $autovacs = [];
        while (!$_autovacs->EOF) {
            $_ = [
                'nspname' => $_autovacs->fields['nspname'],
                'relname' => $_autovacs->fields['relname'],
            ];

            foreach (explode(',', $_autovacs->fields['reloptions']) as $var) {
                list($o, $v) = explode('=', $var);
                $_[$o]       = $v;
            }

            $autovacs[] = $_;

            $_autovacs->moveNext();
        }

        return new \PHPPgAdmin\ArrayRecordSet($autovacs);
    }

    /**
     * Get the fields for uniquely identifying a row in a table
     *
     * @param $table The table for which to retrieve the identifier
     * @return An array mapping attribute number to attribute name, empty for no identifiers
     * @return -1 error
     */
    public function getRowIdentifier($table)
    {
        $oldtable = $table;
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $this->clean($table);

        $status = $this->beginTransaction();
        if ($status != 0) {
            return -1;
        }

        // Get the first primary or unique index (sorting primary keys first) that
        // is NOT a partial index.
        $sql = "
			SELECT indrelid, indkey
			FROM pg_catalog.pg_index
			WHERE indisunique AND indrelid=(
				SELECT oid FROM pg_catalog.pg_class
				WHERE relname='{$table}' AND relnamespace=(
					SELECT oid FROM pg_catalog.pg_namespace
					WHERE nspname='{$c_schema}'
				)
			) AND indpred IS NULL AND indexprs IS NULL
			ORDER BY indisprimary DESC LIMIT 1";
        $rs = $this->selectSet($sql);

        // If none, check for an OID column.  Even though OIDs can be duplicated, the edit and delete row
        // functions check that they're only modiying a single row.  Otherwise, return empty array.
        if ($rs->recordCount() == 0) {
            // Check for OID column
            $temp = [];
            if ($this->hasObjectID($table)) {
                $temp = ['oid'];
            }
            $this->endTransaction();

            return $temp;
        } // Otherwise find the names of the keys

        $attnames = $this->getAttributeNames($oldtable, explode(' ', $rs->fields['indkey']));
        if (!is_array($attnames)) {
            $this->rollbackTransaction();

            return -1;
        }

        $this->endTransaction();

        return $attnames;
    }

    /**
     * Adds a new row to a table
     *
     * @param $table  The table in which to insert
     * @param $fields Array of given field in values
     * @param $values Array of new values for the row
     * @param $nulls  An array mapping column => something if it is to be null
     * @param $format An array of the data type (VALUE or EXPRESSION)
     * @param $types  An array of field types
     * @return int|\PHPPgAdmin\Database\A 0 success
     */
    public function insertRow($table, $fields, $values, $nulls, $format, $types)
    {

        if (!is_array($fields) || !is_array($values) || !is_array($nulls)
            || !is_array($format) || !is_array($types)
            || (count($fields) != count($values))
        ) {
            return -1;
        }

// Build clause
        if (count($values) > 0) {
            // Escape all field names
            $fields   = array_map(['\PHPPgAdmin\Database\Postgres', 'fieldClean'], $fields);
            $f_schema = $this->_schema;
            $this->fieldClean($table);
            $this->fieldClean($f_schema);

            $sql = '';
            foreach ($values as $i => $value) {

                // Handle NULL values
                if (isset($nulls[$i])) {
                    $sql .= ',NULL';
                } else {
                    $sql .= ',' . $this->formatValue($types[$i], $format[$i], $value);
                }
            }

            $sql = "INSERT INTO \"{$f_schema}\".\"{$table}\" (\"" . implode('","', $fields) . '")
                VALUES (' . substr($sql, 1) . ')';

            return $this->execute($sql);
        }

        return -1;
    }

    /**
     * Formats a value or expression for sql purposes
     *
     * @param $type   The type of the field
     * @param $format VALUE or EXPRESSION
     * @param $value  The actual value entered in the field.  Can be NULL
     * @return The suitably quoted and escaped value.
     */
    public function formatValue($type, $format, $value)
    {
        switch ($type) {
            case 'bool':
            case 'boolean':
                if ($value == 't') {
                    return 'TRUE';
                }

                if ($value == 'f') {
                    return 'FALSE';
                } elseif ($value == '') {
                    return 'NULL';
                } else {
                    return $value;
                }

                break;
            default:
                // Checking variable fields is difficult as there might be a size
                // attribute...
                if (strpos($type, 'time') === 0) {
                    // Assume it's one of the time types...
                    if ($value == '') {
                        return "''";
                    }

                    if (strcasecmp($value, 'CURRENT_TIMESTAMP') == 0
                        || strcasecmp($value, 'CURRENT_TIME') == 0
                        || strcasecmp($value, 'CURRENT_DATE') == 0
                        || strcasecmp($value, 'LOCALTIME') == 0
                        || strcasecmp($value, 'LOCALTIMESTAMP') == 0) {
                        return $value;
                    } elseif ($format == 'EXPRESSION') {
                        return $value;
                    } else {
                        $this->clean($value);

                        return "'{$value}'";
                    }
                } else {
                    if ($format == 'VALUE') {
                        $this->clean($value);

                        return "'{$value}'";
                    }

                    return $value;
                }
        }
    }

    // View functions

    /**
     * Updates a row in a table
     *
     * @param $table  The table in which to update
     * @param $vars   An array mapping new values for the row
     * @param $nulls  An array mapping column => something if it is to be null
     * @param $format An array of the data type (VALUE or EXPRESSION)
     * @param $types  An array of field types
     * @param $keyarr An array mapping column => value to update
     * @return bool|int 0 success
     */
    public function editRow($table, $vars, $nulls, $format, $types, $keyarr)
    {
        if (!is_array($vars) || !is_array($nulls) || !is_array($format) || !is_array($types)) {
            return -1;
        }

        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($table);

        // Build clause
        if (sizeof($vars) > 0) {

            foreach ($vars as $key => $value) {
                $this->fieldClean($key);

                // Handle NULL values
                if (isset($nulls[$key])) {
                    $tmp = 'NULL';
                } else {
                    $tmp = $this->formatValue($types[$key], $format[$key], $value);
                }

                if (isset($sql)) {
                    $sql .= ", \"{$key}\"={$tmp}";
                } else {
                    $sql = "UPDATE \"{$f_schema}\".\"{$table}\" SET \"{$key}\"={$tmp}";
                }
            }
            $first = true;
            foreach ($keyarr as $k => $v) {
                $this->fieldClean($k);
                $this->clean($v);
                if ($first) {
                    $sql .= " WHERE \"{$k}\"='{$v}'";
                    $first = false;
                } else {
                    $sql .= " AND \"{$k}\"='{$v}'";
                }
            }
        }

        // Begin transaction.  We do this so that we can ensure only one row is
        // edited
        $status = $this->beginTransaction();
        if ($status != 0) {
            $this->rollbackTransaction();

            return -1;
        }

        $status = $this->execute($sql);
        if ($status != 0) {
            // update failed
            $this->rollbackTransaction();

            return -1;
        }

        if ($this->conn->Affected_Rows() != 1) {
            // more than one row could be updated
            $this->rollbackTransaction();

            return -2;
        }

        // End transaction
        return $this->endTransaction();
    }

    /**
     * Delete a row from a table
     *
     * @param      $table The table from which to delete
     * @param      $key   An array mapping column => value to delete
     * @param bool $schema
     * @return bool|int 0 success
     */
    public function deleteRow($table, $key, $schema = false)
    {
        if (!is_array($key)) {
            return -1;
        }

// Begin transaction.  We do this so that we can ensure only one row is
        // deleted
        $status = $this->beginTransaction();
        if ($status != 0) {
            $this->rollbackTransaction();

            return -1;
        }

        if ($schema === false) {
            $schema = $this->_schema;
        }

        $status = $this->delete($table, $key, $schema);
        if ($status != 0 || $this->conn->Affected_Rows() != 1) {
            $this->rollbackTransaction();

            return -2;
        }

        // End transaction
        return $this->endTransaction();
    }

    /**
     * Returns all sequences in the current database
     *
     * @param bool $all
     * @return \PHPPgAdmin\Database\A recordset
     */
    public function getSequences($all = false)
    {
        if ($all) {
            // Exclude pg_catalog and information_schema tables
            $sql = "SELECT n.nspname, c.relname AS seqname, u.usename AS seqowner
				FROM pg_catalog.pg_class c, pg_catalog.pg_user u, pg_catalog.pg_namespace n
				WHERE c.relowner=u.usesysid AND c.relnamespace=n.oid
				AND c.relkind = 'S'
				AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
				ORDER BY nspname, seqname";
        } else {
            $c_schema = $this->_schema;
            $this->clean($c_schema);
            $sql = "SELECT c.relname AS seqname, u.usename AS seqowner, pg_catalog.obj_description(c.oid, 'pg_class') AS seqcomment,
				(SELECT spcname FROM pg_catalog.pg_tablespace pt WHERE pt.oid=c.reltablespace) AS tablespace
				FROM pg_catalog.pg_class c, pg_catalog.pg_user u, pg_catalog.pg_namespace n
				WHERE c.relowner=u.usesysid AND c.relnamespace=n.oid
				AND c.relkind = 'S' AND n.nspname='{$c_schema}' ORDER BY seqname";
        }

        return $this->selectSet($sql);
    }

    /**
     * Execute nextval on a given sequence
     *
     * @param $sequence Sequence name
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function nextvalSequence($sequence)
    {
        /* This double-cleaning is deliberate */
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->clean($f_schema);
        $this->fieldClean($sequence);
        $this->clean($sequence);

        $sql = "SELECT pg_catalog.NEXTVAL('\"{$f_schema}\".\"{$sequence}\"')";

        return $this->execute($sql);
    }

    /**
     * Execute setval on a given sequence
     *
     * @param $sequence  Sequence name
     * @param $nextvalue The next value
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function setvalSequence($sequence, $nextvalue)
    {
        /* This double-cleaning is deliberate */
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->clean($f_schema);
        $this->fieldClean($sequence);
        $this->clean($sequence);
        $this->clean($nextvalue);

        $sql = "SELECT pg_catalog.SETVAL('\"{$f_schema}\".\"{$sequence}\"', '{$nextvalue}')";

        return $this->execute($sql);
    }

    /**
     * Restart a given sequence to its start value
     *
     * @param $sequence Sequence name
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function restartSequence($sequence)
    {

        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($sequence);

        $sql = "ALTER SEQUENCE \"{$f_schema}\".\"{$sequence}\" RESTART;";

        return $this->execute($sql);
    }

    /**
     * Resets a given sequence to min value of sequence
     *
     * @param $sequence Sequence name
     * @return int|\PHPPgAdmin\Database\A 0 success
     */
    public function resetSequence($sequence)
    {
        // Get the minimum value of the sequence
        $seq = $this->getSequence($sequence);
        if ($seq->recordCount() != 1) {
            return -1;
        }

        $minvalue = $seq->fields['min_value'];

        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        /* This double-cleaning is deliberate */
        $this->fieldClean($sequence);
        $this->clean($sequence);

        $sql = "SELECT pg_catalog.SETVAL('\"{$f_schema}\".\"{$sequence}\"', {$minvalue})";

        return $this->execute($sql);
    }

    /**
     * Returns properties of a single sequence
     *
     * @param $sequence Sequence name
     * @return A recordset
     */
    public function getSequence($sequence)
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $c_sequence = $sequence;
        $this->fieldClean($sequence);
        $this->clean($c_sequence);

        $sql = "
			SELECT c.relname AS seqname, s.*,
				pg_catalog.obj_description(s.tableoid, 'pg_class') AS seqcomment,
				u.usename AS seqowner, n.nspname
			FROM \"{$sequence}\" AS s, pg_catalog.pg_class c, pg_catalog.pg_user u, pg_catalog.pg_namespace n
			WHERE c.relowner=u.usesysid AND c.relnamespace=n.oid
				AND c.relname = '{$c_sequence}' AND c.relkind = 'S' AND n.nspname='{$c_schema}'
				AND n.oid = c.relnamespace";

        return $this->selectSet($sql);
    }

    /**
     * Creates a new sequence
     *
     * @param $sequence    Sequence name
     * @param $increment   The increment
     * @param $minvalue    The min value
     * @param $maxvalue    The max value
     * @param $startvalue  The starting value
     * @param $cachevalue  The cache value
     * @param $cycledvalue True if cycled, false otherwise
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function createSequence(
        $sequence,
        $increment,
        $minvalue,
        $maxvalue,
        $startvalue,
        $cachevalue,
        $cycledvalue
    ) {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($sequence);
        $this->clean($increment);
        $this->clean($minvalue);
        $this->clean($maxvalue);
        $this->clean($startvalue);
        $this->clean($cachevalue);

        $sql = "CREATE SEQUENCE \"{$f_schema}\".\"{$sequence}\"";
        if ($increment != '') {
            $sql .= " INCREMENT {$increment}";
        }

        if ($minvalue != '') {
            $sql .= " MINVALUE {$minvalue}";
        }

        if ($maxvalue != '') {
            $sql .= " MAXVALUE {$maxvalue}";
        }

        if ($startvalue != '') {
            $sql .= " START {$startvalue}";
        }

        if ($cachevalue != '') {
            $sql .= " CACHE {$cachevalue}";
        }

        if ($cycledvalue) {
            $sql .= ' CYCLE';
        }

        return $this->execute($sql);
    }

    /**
     * Alters a sequence
     *
     * @param $sequence     The name of the sequence
     * @param $name         The new name for the sequence
     * @param $comment      The comment on the sequence
     * @param $owner        The new owner for the sequence
     * @param $schema       The new schema for the sequence
     * @param $increment    The increment
     * @param $minvalue     The min value
     * @param $maxvalue     The max value
     * @param $restartvalue The starting value
     * @param $cachevalue   The cache value
     * @param $cycledvalue  True if cycled, false otherwise
     * @param $startvalue   The sequence start value when issueing a restart
     * @return bool|int 0 success
     */
    public function alterSequence(
        $sequence,
        $name,
        $comment,
        $owner = null,
        $schema = null,
        $increment = null,
        $minvalue = null,
        $maxvalue = null,
        $restartvalue = null,
        $cachevalue = null,
        $cycledvalue = null,
        $startvalue = null
    ) {

        $this->fieldClean($sequence);

        $data = $this->getSequence($sequence);

        if ($data->recordCount() != 1) {
            return -2;
        }

        $status = $this->beginTransaction();
        if ($status != 0) {
            $this->rollbackTransaction();

            return -1;
        }

        $status = $this->_alterSequence($data, $name, $comment, $owner, $schema, $increment,
            $minvalue, $maxvalue, $restartvalue, $cachevalue, $cycledvalue, $startvalue);

        if ($status != 0) {
            $this->rollbackTransaction();

            return $status;
        }

        return $this->endTransaction();
    }

    /**
     * Protected method which alter a sequence
     * SHOULDN'T BE CALLED OUTSIDE OF A TRANSACTION
     *
     * @param $seqrs        The sequence recordSet returned by getSequence()
     * @param $name         The new name for the sequence
     * @param $comment      The comment on the sequence
     * @param $owner        The new owner for the sequence
     * @param $schema       The new schema for the sequence
     * @param $increment    The increment
     * @param $minvalue     The min value
     * @param $maxvalue     The max value
     * @param $restartvalue The starting value
     * @param $cachevalue   The cache value
     * @param $cycledvalue  True if cycled, false otherwise
     * @param $startvalue   The sequence start value when issueing a restart
     * @return int 0 success
     */
    protected function _alterSequence(
        $seqrs,
        $name,
        $comment,
        $owner,
        $schema,
        $increment,
        $minvalue,
        $maxvalue,
        $restartvalue,
        $cachevalue,
        $cycledvalue,
        $startvalue
    ) {

        $this->fieldArrayClean($seqrs->fields);

        // Comment
        $status = $this->setComment('SEQUENCE', $seqrs->fields['seqname'], '', $comment);
        if ($status != 0) {
            return -4;
        }

        // Owner
        $this->fieldClean($owner);
        $status = $this->alterSequenceOwner($seqrs, $owner);
        if ($status != 0) {
            return -5;
        }

        // Props
        $this->clean($increment);
        $this->clean($minvalue);
        $this->clean($maxvalue);
        $this->clean($restartvalue);
        $this->clean($cachevalue);
        $this->clean($cycledvalue);
        $this->clean($startvalue);
        $status = $this->alterSequenceProps($seqrs, $increment, $minvalue,
            $maxvalue, $restartvalue, $cachevalue, $cycledvalue, $startvalue);
        if ($status != 0) {
            return -6;
        }

        // Rename
        $this->fieldClean($name);
        $status = $this->alterSequenceName($seqrs, $name);
        if ($status != 0) {
            return -3;
        }

        // Schema
        $this->clean($schema);
        $status = $this->alterSequenceSchema($seqrs, $schema);
        if ($status != 0) {
            return -7;
        }

        return 0;
    }

    // Index functions

    /**
     * Alter a sequence's owner
     *
     * @param $seqrs The sequence RecordSet returned by getSequence()
     * @param $owner
     * @return int|\PHPPgAdmin\Database\A 0 success
     * @internal param \PHPPgAdmin\Database\The $name new owner for the sequence
     */
    public function alterSequenceOwner($seqrs, $owner)
    {
        // If owner has been changed, then do the alteration.  We are
        // careful to avoid this generally as changing owner is a
        // superuser only function.
        /* vars are cleaned in _alterSequence */
        if (!empty($owner) && ($seqrs->fields['seqowner'] != $owner)) {
            $f_schema = $this->_schema;
            $this->fieldClean($f_schema);
            $sql = "ALTER SEQUENCE \"{$f_schema}\".\"{$seqrs->fields['seqname']}\" OWNER TO \"{$owner}\"";

            return $this->execute($sql);
        }

        return 0;
    }

    /**
     * Alter a sequence's properties
     *
     * @param $seqrs        The sequence RecordSet returned by getSequence()
     * @param $increment    The sequence incremental value
     * @param $minvalue     The sequence minimum value
     * @param $maxvalue     The sequence maximum value
     * @param $restartvalue The sequence current value
     * @param $cachevalue   The sequence cache value
     * @param $cycledvalue  Sequence can cycle ?
     * @param $startvalue   The sequence start value when issueing a restart
     * @return int|\PHPPgAdmin\Database\A 0 success
     */
    public function alterSequenceProps(
        $seqrs,
        $increment,
        $minvalue,
        $maxvalue,
        $restartvalue,
        $cachevalue,
        $cycledvalue,
        $startvalue
    ) {

        $sql = '';
        /* vars are cleaned in _alterSequence */
        if (!empty($increment) && ($increment != $seqrs->fields['increment_by'])) {
            $sql .= " INCREMENT {$increment}";
        }

        if (!empty($minvalue) && ($minvalue != $seqrs->fields['min_value'])) {
            $sql .= " MINVALUE {$minvalue}";
        }

        if (!empty($maxvalue) && ($maxvalue != $seqrs->fields['max_value'])) {
            $sql .= " MAXVALUE {$maxvalue}";
        }

        if (!empty($restartvalue) && ($restartvalue != $seqrs->fields['last_value'])) {
            $sql .= " RESTART {$restartvalue}";
        }

        if (!empty($cachevalue) && ($cachevalue != $seqrs->fields['cache_value'])) {
            $sql .= " CACHE {$cachevalue}";
        }

        if (!empty($startvalue) && ($startvalue != $seqrs->fields['start_value'])) {
            $sql .= " START {$startvalue}";
        }

        // toggle cycle yes/no
        if (!is_null($cycledvalue)) {
            $sql .= (!$cycledvalue ? ' NO ' : '') . ' CYCLE';
        }

        if ($sql != '') {
            $f_schema = $this->_schema;
            $this->fieldClean($f_schema);
            $sql = "ALTER SEQUENCE \"{$f_schema}\".\"{$seqrs->fields['seqname']}\" {$sql}";

            return $this->execute($sql);
        }

        return 0;
    }

    /**
     * Rename a sequence
     *
     * @param $seqrs The sequence RecordSet returned by getSequence()
     * @param $name  The new name for the sequence
     * @return int|\PHPPgAdmin\Database\A 0 success
     */
    public function alterSequenceName($seqrs, $name)
    {
        /* vars are cleaned in _alterSequence */
        if (!empty($name) && ($seqrs->fields['seqname'] != $name)) {
            $f_schema = $this->_schema;
            $this->fieldClean($f_schema);
            $sql    = "ALTER SEQUENCE \"{$f_schema}\".\"{$seqrs->fields['seqname']}\" RENAME TO \"{$name}\"";
            $status = $this->execute($sql);
            if ($status == 0) {
                $seqrs->fields['seqname'] = $name;
            } else {
                return $status;
            }
        }

        return 0;
    }

    /**
     * Alter a sequence's schema
     *
     * @param $seqrs The sequence RecordSet returned by getSequence()
     * @param $schema
     * @return int|\PHPPgAdmin\Database\A 0 success
     * @internal param \PHPPgAdmin\Database\The $name new schema for the sequence
     */
    public function alterSequenceSchema($seqrs, $schema)
    {
        /* vars are cleaned in _alterSequence */
        if (!empty($schema) && ($seqrs->fields['nspname'] != $schema)) {
            $f_schema = $this->_schema;
            $this->fieldClean($f_schema);
            $sql = "ALTER SEQUENCE \"{$f_schema}\".\"{$seqrs->fields['seqname']}\" SET SCHEMA {$schema}";

            return $this->execute($sql);
        }

        return 0;
    }

    /**
     * Drops a given sequence
     *
     * @param $sequence Sequence name
     * @param $cascade  True to cascade drop, false to restrict
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function dropSequence($sequence, $cascade)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($sequence);

        $sql = "DROP SEQUENCE \"{$f_schema}\".\"{$sequence}\"";
        if ($cascade) {
            $sql .= ' CASCADE';
        }

        return $this->execute($sql);
    }

    /**
     * Returns a list of all views in the database
     *
     * @return All views
     */
    public function getViews()
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $sql = "
			SELECT c.relname, pg_catalog.pg_get_userbyid(c.relowner) AS relowner,
				pg_catalog.obj_description(c.oid, 'pg_class') AS relcomment
			FROM pg_catalog.pg_class c
				LEFT JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace)
			WHERE (n.nspname='{$c_schema}') AND (c.relkind = 'v'::\"char\")
			ORDER BY relname";

        return $this->selectSet($sql);
    }

    // Constraint functions

    /**
     * Returns a list of all views in the database
     *
     * @return All views
     */
    public function getMaterializedViews()
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $sql = "
			SELECT c.relname, pg_catalog.pg_get_userbyid(c.relowner) AS relowner,
				pg_catalog.obj_description(c.oid, 'pg_class') AS relcomment
			FROM pg_catalog.pg_class c
				LEFT JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace)
			WHERE (n.nspname='{$c_schema}') AND (c.relkind = 'm'::\"char\")
			ORDER BY relname";

        return $this->selectSet($sql);
    }

    /**
     * Updates a view.
     *
     * @param $viewname   The name fo the view to update
     * @param $definition The new definition for the view
     * @param $comment
     * @return bool|int 0 success
     */
    public function setView($viewname, $definition, $comment)
    {
        return $this->createView($viewname, $definition, true, $comment);
    }

    /**
     * Creates a new view.
     *
     * @param $viewname   The name of the view to create
     * @param $definition The definition for the new view
     * @param $replace    True to replace the view, false otherwise
     * @param $comment
     * @return bool|int 0 success
     */
    public function createView($viewname, $definition, $replace, $comment)
    {
        $status = $this->beginTransaction();
        if ($status != 0) {
            return -1;
        }

        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($viewname);

        // Note: $definition not cleaned

        $sql = 'CREATE ';
        if ($replace) {
            $sql .= 'OR REPLACE ';
        }

        $sql .= "VIEW \"{$f_schema}\".\"{$viewname}\" AS {$definition}";

        $status = $this->execute($sql);
        if ($status) {
            $this->rollbackTransaction();

            return -1;
        }

        if ($comment != '') {
            $status = $this->setComment('VIEW', $viewname, '', $comment);
            if ($status) {
                $this->rollbackTransaction();

                return -1;
            }
        }

        return $this->endTransaction();
    }

    /**
     * Alter view properties
     *
     * @param $view    The name of the view
     * @param $name    The new name for the view
     * @param $owner   The new owner for the view
     * @param $schema  The new schema for the view
     * @param $comment The comment on the view
     * @return bool|int 0 success
     */
    public function alterView($view, $name, $owner, $schema, $comment)
    {

        $data = $this->getView($view);
        if ($data->recordCount() != 1) {
            return -2;
        }

        $status = $this->beginTransaction();
        if ($status != 0) {
            $this->rollbackTransaction();

            return -1;
        }

        $status = $this->_alterView($data, $name, $owner, $schema, $comment);

        if ($status != 0) {
            $this->rollbackTransaction();

            return $status;
        }

        return $this->endTransaction();
    }

    /**
     * Returns all details for a particular view
     *
     * @param $view The name of the view to retrieve
     * @return View info
     */
    public function getView($view)
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $this->clean($view);

        $sql = "
			SELECT c.relname, n.nspname, pg_catalog.pg_get_userbyid(c.relowner) AS relowner,
				pg_catalog.pg_get_viewdef(c.oid, true) AS vwdefinition,
				pg_catalog.obj_description(c.oid, 'pg_class') AS relcomment
			FROM pg_catalog.pg_class c
				LEFT JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace)
			WHERE (c.relname = '{$view}') AND n.nspname='{$c_schema}'";

        return $this->selectSet($sql);
    }

    /**
     * Protected method which alter a view
     * SHOULDN'T BE CALLED OUTSIDE OF A TRANSACTION
     *
     * @param $vwrs    The view recordSet returned by getView()
     * @param $name    The new name for the view
     * @param $owner   The new owner for the view
     * @param $schema
     * @param $comment The comment on the view
     * @return int 0 success
     */
    protected function _alterView($vwrs, $name, $owner, $schema, $comment)
    {

        $this->fieldArrayClean($vwrs->fields);

        // Comment
        if ($this->setComment('VIEW', $vwrs->fields['relname'], '', $comment) != 0) {
            return -4;
        }

        // Owner
        $this->fieldClean($owner);
        $status = $this->alterViewOwner($vwrs, $owner);
        if ($status != 0) {
            return -5;
        }

        // Rename
        $this->fieldClean($name);
        $status = $this->alterViewName($vwrs, $name);
        if ($status != 0) {
            return -3;
        }

        // Schema
        $this->fieldClean($schema);
        $status = $this->alterViewSchema($vwrs, $schema);
        if ($status != 0) {
            return -6;
        }

        return 0;
    }

    /**
     * Alter a view's owner
     *
     * @param      $vwrs The view recordSet returned by getView()
     * @param null $owner
     * @return int|\PHPPgAdmin\Database\A 0 success
     * @internal param \PHPPgAdmin\Database\The $name new view's owner
     */
    public function alterViewOwner($vwrs, $owner = null)
    {
        /* $vwrs and $owner are cleaned in _alterView */
        if ((!empty($owner)) && ($vwrs->fields['relowner'] != $owner)) {
            $f_schema = $this->_schema;
            $this->fieldClean($f_schema);
            // If owner has been changed, then do the alteration.  We are
            // careful to avoid this generally as changing owner is a
            // superuser only function.
            $sql = "ALTER TABLE \"{$f_schema}\".\"{$vwrs->fields['relname']}\" OWNER TO \"{$owner}\"";

            return $this->execute($sql);
        }

        return 0;
    }

    /**
     * Rename a view
     *
     * @param $vwrs The view recordSet returned by getView()
     * @param $name The new view's name
     * @return int|\PHPPgAdmin\Database\A 0 success
     */
    public function alterViewName($vwrs, $name)
    {
        // Rename (only if name has changed)
        /* $vwrs and $name are cleaned in _alterView */
        if (!empty($name) && ($name != $vwrs->fields['relname'])) {
            $f_schema = $this->_schema;
            $this->fieldClean($f_schema);
            $sql    = "ALTER VIEW \"{$f_schema}\".\"{$vwrs->fields['relname']}\" RENAME TO \"{$name}\"";
            $status = $this->execute($sql);
            if ($status == 0) {
                $vwrs->fields['relname'] = $name;
            } else {
                return $status;
            }
        }

        return 0;
    }

    /**
     * Alter a view's schema
     *
     * @param $vwrs The view recordSet returned by getView()
     * @param $schema
     * @return int|\PHPPgAdmin\Database\A 0 success
     * @internal param \PHPPgAdmin\Database\The $name new view's schema
     */
    public function alterViewSchema($vwrs, $schema)
    {
        /* $vwrs and $schema are cleaned in _alterView */
        if (!empty($schema) && ($vwrs->fields['nspname'] != $schema)) {
            $f_schema = $this->_schema;
            $this->fieldClean($f_schema);
            // If tablespace has been changed, then do the alteration.  We
            // don't want to do this unnecessarily.
            $sql = "ALTER TABLE \"{$f_schema}\".\"{$vwrs->fields['relname']}\" SET SCHEMA \"{$schema}\"";

            return $this->execute($sql);
        }

        return 0;
    }

    /**
     * Drops a view.
     *
     * @param $viewname The name of the view to drop
     * @param $cascade  True to cascade drop, false to restrict
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function dropView($viewname, $cascade)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($viewname);

        $sql = "DROP VIEW \"{$f_schema}\".\"{$viewname}\"";
        if ($cascade) {
            $sql .= ' CASCADE';
        }

        return $this->execute($sql);
    }

    // Domain functions

    /**
     * test if a table has been clustered on an index
     *
     * @param $table The table to test
     *
     * @return true if the table has been already clustered
     */
    public function alreadyClustered($table)
    {

        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $this->clean($table);

        $sql = "SELECT i.indisclustered
			FROM pg_catalog.pg_class c, pg_catalog.pg_index i
			WHERE c.relname = '{$table}'
				AND c.oid = i.indrelid AND i.indisclustered
				AND c.relnamespace = (SELECT oid FROM pg_catalog.pg_namespace
					WHERE nspname='{$c_schema}')
				";

        $v = $this->selectSet($sql);

        return !($v->recordCount() == 0);
    }

    /**
     * Creates an index
     *
     * @param $name       The index name
     * @param $table      The table on which to add the index
     * @param $columns    An array of columns that form the index
     *                    or a string expression for a functional index
     * @param $type       The index type
     * @param $unique     True if unique, false otherwise
     * @param $where      Index predicate ('' for none)
     * @param $tablespace The tablespaces ('' means none/default)
     * @param $concurrently
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function createIndex($name, $table, $columns, $type, $unique, $where, $tablespace, $concurrently)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($name);
        $this->fieldClean($table);

        $sql = 'CREATE';
        if ($unique) {
            $sql .= ' UNIQUE';
        }

        $sql .= ' INDEX';
        if ($concurrently) {
            $sql .= ' CONCURRENTLY';
        }

        $sql .= " \"{$name}\" ON \"{$f_schema}\".\"{$table}\" USING {$type} ";

        if (is_array($columns)) {
            $this->arrayClean($columns);
            $sql .= '("' . implode('","', $columns) . '")';
        } else {
            $sql .= '(' . $columns . ')';
        }

        // Tablespace
        if ($this->hasTablespaces() && $tablespace != '') {
            $this->fieldClean($tablespace);
            $sql .= " TABLESPACE \"{$tablespace}\"";
        }

        // Predicate
        if (trim($where) != '') {
            $sql .= " WHERE ({$where})";
        }

        return $this->execute($sql);
    }

    /**
     * Removes an index from the database
     *
     * @param $index   The index to drop
     * @param $cascade True to cascade drop, false to restrict
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function dropIndex($index, $cascade)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($index);

        $sql = "DROP INDEX \"{$f_schema}\".\"{$index}\"";
        if ($cascade) {
            $sql .= ' CASCADE';
        }

        return $this->execute($sql);
    }

    /**
     * Rebuild indexes
     *
     * @param                              $type  'DATABASE' or 'TABLE' or 'INDEX'
     * @param                              $name  The name of the specific database, table, or index to be reindexed
     * @param bool|\PHPPgAdmin\Database\If $force If true, recreates indexes forcedly in PostgreSQL 7.0-7.1, forces rebuild of system indexes in
     *                                            7.2-7.3, ignored in >=7.4
     * @return int|\PHPPgAdmin\Database\A
     */
    public function reindex($type, $name, $force = false)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($name);
        switch ($type) {
            case 'DATABASE':
                $sql = "REINDEX {$type} \"{$name}\"";
                if ($force) {
                    $sql .= ' FORCE';
                }

                break;
            case 'TABLE':
            case 'INDEX':
                $sql = "REINDEX {$type} \"{$f_schema}\".\"{$name}\"";
                if ($force) {
                    $sql .= ' FORCE';
                }

                break;
            default:
                return -1;
        }

        return $this->execute($sql);
    }

    /**
     * Clusters an index
     *
     * @param \PHPPgAdmin\Database\The|string $table The table the index is on
     * @param \PHPPgAdmin\Database\The|string $index The name of the index
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function clusterIndex($table = '', $index = '')
    {

        $sql = 'CLUSTER';

        // We don't bother with a transaction here, as there's no point rolling
        // back an expensive cluster if a cheap analyze fails for whatever reason

        if (!empty($table)) {
            $f_schema = $this->_schema;
            $this->fieldClean($f_schema);
            $this->fieldClean($table);
            $sql .= " \"{$f_schema}\".\"{$table}\"";

            if (!empty($index)) {
                $this->fieldClean($index);
                $sql .= " USING \"{$index}\"";
            }
        }

        return $this->execute($sql);
    }

    /**
     * Returns a list of all constraints on a table,
     * including constraint name, definition, related col and referenced namespace,
     * table and col if needed
     *
     * @param $table the table where we are looking for fk
     * @return a recordset
     */
    public function getConstraintsWithFields($table)
    {

        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $this->clean($table);

        // get the max number of col used in a constraint for the table
        $sql = "SELECT DISTINCT
			max(SUBSTRING(array_dims(c.conkey) FROM  \$patern\$^\\[.*:(.*)\\]$\$patern\$)) as nb
		FROM pg_catalog.pg_constraint AS c
			JOIN pg_catalog.pg_class AS r ON (c.conrelid=r.oid)
			JOIN pg_catalog.pg_namespace AS ns ON (r.relnamespace=ns.oid)
		WHERE
			r.relname = '{$table}' AND ns.nspname='{$c_schema}'";

        $rs = $this->selectSet($sql);

        if ($rs->EOF) {
            $max_col = 0;
        } else {
            $max_col = $rs->fields['nb'];
        }

        $sql = '
			SELECT
				c.oid AS conid, c.contype, c.conname, pg_catalog.pg_get_constraintdef(c.oid, true) AS consrc,
				ns1.nspname as p_schema, r1.relname as p_table, ns2.nspname as f_schema,
				r2.relname as f_table, f1.attname as p_field, f1.attnum AS p_attnum, f2.attname as f_field,
				f2.attnum AS f_attnum, pg_catalog.obj_description(c.oid, \'pg_constraint\') AS constcomment,
				c.conrelid, c.confrelid
			FROM
				pg_catalog.pg_constraint AS c
				JOIN pg_catalog.pg_class AS r1 ON (c.conrelid=r1.oid)
				JOIN pg_catalog.pg_attribute AS f1 ON (f1.attrelid=r1.oid AND (f1.attnum=c.conkey[1]';
        for ($i = 2; $i <= $rs->fields['nb']; $i++) {
            $sql .= " OR f1.attnum=c.conkey[$i]";
        }
        $sql .= '))
				JOIN pg_catalog.pg_namespace AS ns1 ON r1.relnamespace=ns1.oid
				LEFT JOIN (
					pg_catalog.pg_class AS r2 JOIN pg_catalog.pg_namespace AS ns2 ON (r2.relnamespace=ns2.oid)
				) ON (c.confrelid=r2.oid)
				LEFT JOIN pg_catalog.pg_attribute AS f2 ON
					(f2.attrelid=r2.oid AND ((c.confkey[1]=f2.attnum AND c.conkey[1]=f1.attnum)';
        for ($i = 2; $i <= $rs->fields['nb']; $i++) {
            $sql .= " OR (c.confkey[$i]=f2.attnum AND c.conkey[$i]=f1.attnum)";
        }

        $sql .= sprintf("))
			WHERE
				r1.relname = '%s' AND ns1.nspname='%s'
			ORDER BY 1", $table, $c_schema);

        return $this->selectSet($sql);
    }

    /**
     * Adds a primary key constraint to a table
     *
     * @param        $table      The table to which to add the primery key
     * @param        $fields     (array) An array of fields over which to add the primary key
     * @param string $name       (optional) The name to give the key, otherwise default name is assigned
     * @param string $tablespace (optional) The tablespace for the schema, '' indicates default.
     * @return int|\PHPPgAdmin\Database\A 0 success
     */
    public function addPrimaryKey($table, $fields, $name = '', $tablespace = '')
    {
        if (!is_array($fields) || sizeof($fields) == 0) {
            return -1;
        }

        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($table);
        $this->fieldArrayClean($fields);
        $this->fieldClean($name);
        $this->fieldClean($tablespace);

        $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" ADD ";
        if ($name != '') {
            $sql .= "CONSTRAINT \"{$name}\" ";
        }

        $sql .= 'PRIMARY KEY ("' . join('","', $fields) . '")';

        if ($tablespace != '' && $this->hasTablespaces()) {
            $sql .= " USING INDEX TABLESPACE \"{$tablespace}\"";
        }

        return $this->execute($sql);
    }

    /**
     * Adds a unique constraint to a table
     *
     * @param        $table      The table to which to add the unique key
     * @param        $fields     (array) An array of fields over which to add the unique key
     * @param string $name       (optional) The name to give the key, otherwise default name is assigned
     * @param string $tablespace (optional) The tablespace for the schema, '' indicates default.
     * @return int|\PHPPgAdmin\Database\A 0 success
     */
    public function addUniqueKey($table, $fields, $name = '', $tablespace = '')
    {
        if (!is_array($fields) || sizeof($fields) == 0) {
            return -1;
        }

        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($table);
        $this->fieldArrayClean($fields);
        $this->fieldClean($name);
        $this->fieldClean($tablespace);

        $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" ADD ";
        if ($name != '') {
            $sql .= "CONSTRAINT \"{$name}\" ";
        }

        $sql .= 'UNIQUE ("' . join('","', $fields) . '")';

        if ($tablespace != '' && $this->hasTablespaces()) {
            $sql .= " USING INDEX TABLESPACE \"{$tablespace}\"";
        }

        return $this->execute($sql);
    }

    // Function functions

    /**
     * Adds a check constraint to a table
     *
     * @param        $table      The table to which to add the check
     * @param        $definition The definition of the check
     * @param string $name       (optional) The name to give the check, otherwise default name is assigned
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function addCheckConstraint($table, $definition, $name = '')
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($table);
        $this->fieldClean($name);
        // @@ How the heck do you clean a definition???

        $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" ADD ";
        if ($name != '') {
            $sql .= "CONSTRAINT \"{$name}\" ";
        }

        $sql .= "CHECK ({$definition})";

        return $this->execute($sql);
    }

    /**
     * Drops a check constraint from a table
     *
     * @param $table The table from which to drop the check
     * @param $name  The name of the check to be dropped
     * @return bool|int 0 success
     */
    public function dropCheckConstraint($table, $name)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $c_table = $table;
        $this->fieldClean($table);
        $this->clean($c_table);
        $this->clean($name);

        // Begin transaction
        $status = $this->beginTransaction();
        if ($status != 0) {
            return -2;
        }

        // Properly lock the table
        $sql    = "LOCK TABLE \"{$f_schema}\".\"{$table}\" IN ACCESS EXCLUSIVE MODE";
        $status = $this->execute($sql);
        if ($status != 0) {
            $this->rollbackTransaction();

            return -3;
        }

        // Delete the check constraint
        $sql = "DELETE FROM pg_relcheck WHERE rcrelid=(SELECT oid FROM pg_catalog.pg_class WHERE relname='{$c_table}'
			AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE
			nspname = '{$c_schema}')) AND rcname='{$name}'";
        $status = $this->execute($sql);
        if ($status != 0) {
            $this->rollbackTransaction();

            return -4;
        }

        // Update the pg_class catalog to reflect the new number of checks
        $sql = "UPDATE pg_class SET relchecks=(SELECT COUNT(*) FROM pg_relcheck WHERE
					rcrelid=(SELECT oid FROM pg_catalog.pg_class WHERE relname='{$c_table}'
						AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE
						nspname = '{$c_schema}')))
					WHERE relname='{$c_table}'";
        $status = $this->execute($sql);
        if ($status != 0) {
            $this->rollbackTransaction();

            return -4;
        }

        // Otherwise, close the transaction
        return $this->endTransaction();
    }

    /**
     * Adds a foreign key constraint to a table
     *
     * @param        $table
     * @param        $targschema The schema that houses the target table to which to add the foreign key
     * @param        $targtable  The table to which to add the foreign key
     * @param        $sfields    (array) An array of source fields over which to add the foreign key
     * @param        $tfields    (array) An array of target fields over which to add the foreign key
     * @param        $upd_action The action for updates (eg. RESTRICT)
     * @param        $del_action The action for deletes (eg. RESTRICT)
     * @param        $match      The match type (eg. MATCH FULL)
     * @param        $deferrable The deferrability (eg. NOT DEFERRABLE)
     * @param        $initially
     * @param string $name       (optional) The name to give the key, otherwise default name is assigned
     * @return \PHPPgAdmin\Database\A 0 success
     * @internal param \PHPPgAdmin\Database\The $target table that contains the target columns
     * @internal param \PHPPgAdmin\Database\The $intially initial deferrability (eg. INITIALLY IMMEDIATE)
     */
    public function addForeignKey(
        $table,
        $targschema,
        $targtable,
        $sfields,
        $tfields,
        $upd_action,
        $del_action,
        $match,
        $deferrable,
        $initially,
        $name = ''
    ) {
        if (!is_array($sfields) || sizeof($sfields) == 0 ||
            !is_array($tfields) || sizeof($tfields) == 0) {
            return -1;
        }

        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($table);
        $this->fieldClean($targschema);
        $this->fieldClean($targtable);
        $this->fieldArrayClean($sfields);
        $this->fieldArrayClean($tfields);
        $this->fieldClean($name);

        $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" ADD ";
        if ($name != '') {
            $sql .= "CONSTRAINT \"{$name}\" ";
        }

        $sql .= 'FOREIGN KEY ("' . join('","', $sfields) . '") ';
        // Target table needs to be fully qualified
        $sql .= "REFERENCES \"{$targschema}\".\"{$targtable}\"(\"" . join('","', $tfields) . '") ';
        if ($match != $this->fkmatches[0]) {
            $sql .= " {$match}";
        }

        if ($upd_action != $this->fkactions[0]) {
            $sql .= " ON UPDATE {$upd_action}";
        }

        if ($del_action != $this->fkactions[0]) {
            $sql .= " ON DELETE {$del_action}";
        }

        if ($deferrable != $this->fkdeferrable[0]) {
            $sql .= " {$deferrable}";
        }

        if ($initially != $this->fkinitial[0]) {
            $sql .= " {$initially}";
        }

        return $this->execute($sql);
    }

    /**
     * Removes a constraint from a relation
     *
     * @param $constraint The constraint to drop
     * @param $relation   The relation from which to drop
     * @param $type       The type of constraint (c, f, u or p)
     * @param $cascade    True to cascade drop, false to restrict
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function dropConstraint($constraint, $relation, $type, $cascade)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($constraint);
        $this->fieldClean($relation);

        $sql = "ALTER TABLE \"{$f_schema}\".\"{$relation}\" DROP CONSTRAINT \"{$constraint}\"";
        if ($cascade) {
            $sql .= ' CASCADE';
        }

        return $this->execute($sql);
    }

    /**
     * A function for getting all columns linked by foreign keys given a group of tables
     *
     * @param $tables multi dimensional assoc array that holds schema and table name
     * @return A recordset of linked tables and columns
     * @return -1 $tables isn't an array
     */
    public function getLinkingKeys($tables)
    {
        if (!is_array($tables)) {
            return -1;
        }

        $this->clean($tables[0]['tablename']);
        $this->clean($tables[0]['schemaname']);
        $tables_list        = "'{$tables[0]['tablename']}'";
        $schema_list        = "'{$tables[0]['schemaname']}'";
        $schema_tables_list = "'{$tables[0]['schemaname']}.{$tables[0]['tablename']}'";

        for ($i = 1; $i < sizeof($tables); $i++) {
            $this->clean($tables[$i]['tablename']);
            $this->clean($tables[$i]['schemaname']);
            $tables_list .= ", '{$tables[$i]['tablename']}'";
            $schema_list .= ", '{$tables[$i]['schemaname']}'";
            $schema_tables_list .= ", '{$tables[$i]['schemaname']}.{$tables[$i]['tablename']}'";
        }

        $maxDimension = 1;

        $sql = "
			SELECT DISTINCT
				array_dims(pc.conkey) AS arr_dim,
				pgc1.relname AS p_table
			FROM
				pg_catalog.pg_constraint AS pc,
				pg_catalog.pg_class AS pgc1
			WHERE
				pc.contype = 'f'
				AND (pc.conrelid = pgc1.relfilenode OR pc.confrelid = pgc1.relfilenode)
				AND pgc1.relname IN ($tables_list)
			";

        //parse our output to find the highest dimension of foreign keys since pc.conkey is stored in an array
        $rs = $this->selectSet($sql);
        while (!$rs->EOF) {
            $arrData      = explode(':', $rs->fields['arr_dim']);
            $tmpDimension = intval(substr($arrData[1], 0, strlen($arrData[1] - 1)));
            $maxDimension = $tmpDimension > $maxDimension ? $tmpDimension : $maxDimension;
            $rs->MoveNext();
        }

        //we know the highest index for foreign keys that conkey goes up to, expand for us in an IN query
        $cons_str = '( (pfield.attnum = conkey[1] AND cfield.attnum = confkey[1]) ';
        for ($i = 2; $i <= $maxDimension; $i++) {
            $cons_str .= "OR (pfield.attnum = conkey[{$i}] AND cfield.attnum = confkey[{$i}]) ";
        }
        $cons_str .= ') ';

        $sql = "
			SELECT
				pgc1.relname AS p_table,
				pgc2.relname AS f_table,
				pfield.attname AS p_field,
				cfield.attname AS f_field,
				pgns1.nspname AS p_schema,
				pgns2.nspname AS f_schema
			FROM
				pg_catalog.pg_constraint AS pc,
				pg_catalog.pg_class AS pgc1,
				pg_catalog.pg_class AS pgc2,
				pg_catalog.pg_attribute AS pfield,
				pg_catalog.pg_attribute AS cfield,
				(SELECT oid AS ns_id, nspname FROM pg_catalog.pg_namespace WHERE nspname IN ($schema_list) ) AS pgns1,
 				(SELECT oid AS ns_id, nspname FROM pg_catalog.pg_namespace WHERE nspname IN ($schema_list) ) AS pgns2
			WHERE
				pc.contype = 'f'
				AND pgc1.relnamespace = pgns1.ns_id
 				AND pgc2.relnamespace = pgns2.ns_id
				AND pc.conrelid = pgc1.relfilenode
				AND pc.confrelid = pgc2.relfilenode
				AND pfield.attrelid = pc.conrelid
				AND cfield.attrelid = pc.confrelid
				AND $cons_str
				AND pgns1.nspname || '.' || pgc1.relname IN ($schema_tables_list)
				AND pgns2.nspname || '.' || pgc2.relname IN ($schema_tables_list)
		";

        return $this->selectSet($sql);
    }

    /**
     * Finds the foreign keys that refer to the specified table
     *
     * @param $table The table to find referrers for
     * @return A recordset
     */
    public function getReferrers($table)
    {
        $this->clean($table);

        $status = $this->beginTransaction();
        if ($status != 0) {
            return -1;
        }

        $c_schema = $this->_schema;
        $this->clean($c_schema);

        $sql = "
			SELECT
				pn.nspname,
				pl.relname,
				pc.conname,
				pg_catalog.pg_get_constraintdef(pc.oid) AS consrc
			FROM
				pg_catalog.pg_constraint pc,
				pg_catalog.pg_namespace pn,
				pg_catalog.pg_class pl
			WHERE
				pc.connamespace = pn.oid
				AND pc.conrelid = pl.oid
				AND pc.contype = 'f'
				AND confrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname='{$table}'
					AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace
					WHERE nspname='{$c_schema}'))
			ORDER BY 1,2,3
		";

        return $this->selectSet($sql);
    }

    // Type functions

    /**
     * Gets all information for a single domain
     *
     * @param $domain The name of the domain to fetch
     * @return A recordset
     */
    public function getDomain($domain)
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $this->clean($domain);

        $sql = "
			SELECT
				t.typname AS domname,
				pg_catalog.format_type(t.typbasetype, t.typtypmod) AS domtype,
				t.typnotnull AS domnotnull,
				t.typdefault AS domdef,
				pg_catalog.pg_get_userbyid(t.typowner) AS domowner,
				pg_catalog.obj_description(t.oid, 'pg_type') AS domcomment
			FROM
				pg_catalog.pg_type t
			WHERE
				t.typtype = 'd'
				AND t.typname = '{$domain}'
				AND t.typnamespace = (SELECT oid FROM pg_catalog.pg_namespace
					WHERE nspname = '{$c_schema}')";

        return $this->selectSet($sql);
    }

    /**
     * Return all domains in current schema.  Excludes domain constraints.
     *
     * @return All tables, sorted alphabetically
     */
    public function getDomains()
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);

        $sql = "
			SELECT
				t.typname AS domname,
				pg_catalog.format_type(t.typbasetype, t.typtypmod) AS domtype,
				t.typnotnull AS domnotnull,
				t.typdefault AS domdef,
				pg_catalog.pg_get_userbyid(t.typowner) AS domowner,
				pg_catalog.obj_description(t.oid, 'pg_type') AS domcomment
			FROM
				pg_catalog.pg_type t
			WHERE
				t.typtype = 'd'
				AND t.typnamespace = (SELECT oid FROM pg_catalog.pg_namespace
					WHERE nspname='{$c_schema}')
			ORDER BY t.typname";

        return $this->selectSet($sql);
    }

    /**
     * Get domain constraints
     *
     * @param $domain The name of the domain whose constraints to fetch
     * @return A recordset
     */
    public function getDomainConstraints($domain)
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $this->clean($domain);

        $sql = "
			SELECT
				conname,
				contype,
				pg_catalog.pg_get_constraintdef(oid, true) AS consrc
			FROM
				pg_catalog.pg_constraint
			WHERE
				contypid = (
					SELECT oid FROM pg_catalog.pg_type
					WHERE typname='{$domain}'
						AND typnamespace = (
							SELECT oid FROM pg_catalog.pg_namespace
							WHERE nspname = '{$c_schema}')
				)
			ORDER BY conname";

        return $this->selectSet($sql);
    }

    /**
     * Creates a domain
     *
     * @param $domain  The name of the domain to create
     * @param $type    The base type for the domain
     * @param $length  Optional type length
     * @param $array   True for array type, false otherwise
     * @param $notnull True for NOT NULL, false otherwise
     * @param $default Default value for domain
     * @param $check   A CHECK constraint if there is one
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function createDomain($domain, $type, $length, $array, $notnull, $default, $check)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($domain);

        $sql = "CREATE DOMAIN \"{$f_schema}\".\"{$domain}\" AS ";

        if ($length == '') {
            $sql .= $type;
        } else {
            switch ($type) {
                // Have to account for weird placing of length for with/without
                // time zone types
                case 'timestamp with time zone':
                case 'timestamp without time zone':
                    $qual = substr($type, 9);
                    $sql .= "timestamp({$length}){$qual}";
                    break;
                case 'time with time zone':
                case 'time without time zone':
                    $qual = substr($type, 4);
                    $sql .= "time({$length}){$qual}";
                    break;
                default:
                    $sql .= "{$type}({$length})";
            }
        }

        // Add array qualifier, if requested
        if ($array) {
            $sql .= '[]';
        }

        if ($notnull) {
            $sql .= ' NOT NULL';
        }

        if ($default != '') {
            $sql .= " DEFAULT {$default}";
        }

        if ($this->hasDomainConstraints() && $check != '') {
            $sql .= " CHECK ({$check})";
        }

        return $this->execute($sql);
    }

    public function hasDomainConstraints()
    {
        return true;
    }

    /**
     * Alters a domain
     *
     * @param $domain     The domain to alter
     * @param $domdefault The domain default
     * @param $domnotnull True for NOT NULL, false otherwise
     * @param $domowner   The domain owner
     * @return bool|int 0 success
     */
    public function alterDomain($domain, $domdefault, $domnotnull, $domowner)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($domain);
        $this->fieldClean($domowner);

        $status = $this->beginTransaction();
        if ($status != 0) {
            $this->rollbackTransaction();

            return -1;
        }

        // Default
        if ($domdefault == '') {
            $sql = "ALTER DOMAIN \"{$f_schema}\".\"{$domain}\" DROP DEFAULT";
        } else {
            $sql = "ALTER DOMAIN \"{$f_schema}\".\"{$domain}\" SET DEFAULT {$domdefault}";
        }

        $status = $this->execute($sql);
        if ($status != 0) {
            $this->rollbackTransaction();

            return -2;
        }

        // NOT NULL
        if ($domnotnull) {
            $sql = "ALTER DOMAIN \"{$f_schema}\".\"{$domain}\" SET NOT NULL";
        } else {
            $sql = "ALTER DOMAIN \"{$f_schema}\".\"{$domain}\" DROP NOT NULL";
        }

        $status = $this->execute($sql);
        if ($status != 0) {
            $this->rollbackTransaction();

            return -3;
        }

        // Owner
        $sql = "ALTER DOMAIN \"{$f_schema}\".\"{$domain}\" OWNER TO \"{$domowner}\"";

        $status = $this->execute($sql);
        if ($status != 0) {
            $this->rollbackTransaction();

            return -4;
        }

        return $this->endTransaction();
    }

    /**
     * Drops a domain.
     *
     * @param $domain  The name of the domain to drop
     * @param $cascade True to cascade drop, false to restrict
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function dropDomain($domain, $cascade)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($domain);

        $sql = "DROP DOMAIN \"{$f_schema}\".\"{$domain}\"";
        if ($cascade) {
            $sql .= ' CASCADE';
        }

        return $this->execute($sql);
    }

    /**
     * Adds a check constraint to a domain
     *
     * @param        $domain     The domain to which to add the check
     * @param        $definition The definition of the check
     * @param string $name       (optional) The name to give the check, otherwise default name is assigned
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function addDomainCheckConstraint($domain, $definition, $name = '')
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($domain);
        $this->fieldClean($name);

        $sql = "ALTER DOMAIN \"{$f_schema}\".\"{$domain}\" ADD ";
        if ($name != '') {
            $sql .= "CONSTRAINT \"{$name}\" ";
        }

        $sql .= "CHECK ({$definition})";

        return $this->execute($sql);
    }

    /**
     * Drops a domain constraint
     *
     * @param $domain     The domain from which to remove the constraint
     * @param $constraint The constraint to remove
     * @param $cascade    True to cascade, false otherwise
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function dropDomainConstraint($domain, $constraint, $cascade)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($domain);
        $this->fieldClean($constraint);

        $sql = "ALTER DOMAIN \"{$f_schema}\".\"{$domain}\" DROP CONSTRAINT \"{$constraint}\"";
        if ($cascade) {
            $sql .= ' CASCADE';
        }

        return $this->execute($sql);
    }

    // Rule functions

    /**
     * Returns an array containing a function's properties
     *
     * @param $f The array of data for the function
     * @return An array containing the properties
     */
    public function getFunctionProperties($f)
    {
        $temp = [];

        // Volatility
        if ($f['provolatile'] == 'v') {
            $temp[] = 'VOLATILE';
        } elseif ($f['provolatile'] == 'i') {
            $temp[] = 'IMMUTABLE';
        } elseif ($f['provolatile'] == 's') {
            $temp[] = 'STABLE';
        } else {
            return -1;
        }

        // Null handling
        $f['proisstrict'] = $this->phpBool($f['proisstrict']);
        if ($f['proisstrict']) {
            $temp[] = 'RETURNS NULL ON NULL INPUT';
        } else {
            $temp[] = 'CALLED ON NULL INPUT';
        }

        // Security
        $f['prosecdef'] = $this->phpBool($f['prosecdef']);
        if ($f['prosecdef']) {
            $temp[] = 'SECURITY DEFINER';
        } else {
            $temp[] = 'SECURITY INVOKER';
        }

        return $temp;
    }

    /**
     * Updates (replaces) a function.
     *
     * @param $function_oid The OID of the function
     * @param $funcname     The name of the function to create
     * @param $newname      The new name for the function
     * @param $args         The array of argument types
     * @param $returns      The return type
     * @param $definition   The definition for the new function
     * @param $language     The language the function is written for
     * @param $flags        An array of optional flags
     * @param $setof        True if returns a set, false otherwise
     * @param $funcown
     * @param $newown
     * @param $funcschema
     * @param $newschema
     * @param $cost
     * @param $rows
     * @param $comment      The comment on the function
     * @return bool|int 0 success
     */
    public function setFunction(
        $function_oid,
        $funcname,
        $newname,
        $args,
        $returns,
        $definition,
        $language,
        $flags,
        $setof,
        $funcown,
        $newown,
        $funcschema,
        $newschema,
        $cost,
        $rows,
        $comment
    ) {
        // Begin a transaction
        $status = $this->beginTransaction();
        if ($status != 0) {
            $this->rollbackTransaction();

            return -1;
        }

        // Replace the existing function
        $status = $this->createFunction($funcname, $args, $returns, $definition, $language, $flags, $setof, $cost, $rows, $comment, true);
        if ($status != 0) {
            $this->rollbackTransaction();

            return $status;
        }

        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);

        // Rename the function, if necessary
        $this->fieldClean($newname);
        /* $funcname is escaped in createFunction */
        if ($funcname != $newname) {
            $sql    = "ALTER FUNCTION \"{$f_schema}\".\"{$funcname}\"({$args}) RENAME TO \"{$newname}\"";
            $status = $this->execute($sql);
            if ($status != 0) {
                $this->rollbackTransaction();

                return -5;
            }

            $funcname = $newname;
        }

        // Alter the owner, if necessary
        if ($this->hasFunctionAlterOwner()) {
            $this->fieldClean($newown);
            if ($funcown != $newown) {
                $sql    = "ALTER FUNCTION \"{$f_schema}\".\"{$funcname}\"({$args}) OWNER TO \"{$newown}\"";
                $status = $this->execute($sql);
                if ($status != 0) {
                    $this->rollbackTransaction();

                    return -6;
                }
            }
        }

        // Alter the schema, if necessary
        if ($this->hasFunctionAlterSchema()) {
            $this->fieldClean($newschema);
            /* $funcschema is escaped in createFunction */
            if ($funcschema != $newschema) {
                $sql    = "ALTER FUNCTION \"{$f_schema}\".\"{$funcname}\"({$args}) SET SCHEMA \"{$newschema}\"";
                $status = $this->execute($sql);
                if ($status != 0) {
                    $this->rollbackTransaction();

                    return -7;
                }
            }
        }

        return $this->endTransaction();
    }

    /**
     * Creates a new function.
     *
     * @param      $funcname   The name of the function to create
     * @param      $args       A comma separated string of types
     * @param      $returns    The return type
     * @param      $definition The definition for the new function
     * @param      $language   The language the function is written for
     * @param      $flags      An array of optional flags
     * @param      $setof      True if it returns a set, false otherwise
     * @param      $cost       cost the planner should use in the function execution step
     * @param      $rows       number of rows planner should estimate will be returned
     * @param      $comment    Comment for the function
     * @param bool $replace    (optional) True if OR REPLACE, false for normal
     * @return bool|int 0 success
     */
    public function createFunction($funcname, $args, $returns, $definition, $language, $flags, $setof, $cost, $rows, $comment, $replace = false)
    {

        // Begin a transaction
        $status = $this->beginTransaction();
        if ($status != 0) {
            $this->rollbackTransaction();

            return -1;
        }

        $this->fieldClean($funcname);
        $this->clean($args);
        $this->fieldClean($language);
        $this->arrayClean($flags);
        $this->clean($cost);
        $this->clean($rows);
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);

        $sql = 'CREATE';
        if ($replace) {
            $sql .= ' OR REPLACE';
        }

        $sql .= " FUNCTION \"{$f_schema}\".\"{$funcname}\" (";

        if ($args != '') {
            $sql .= $args;
        }

        // For some reason, the returns field cannot have quotes...
        $sql .= ') RETURNS ';
        if ($setof) {
            $sql .= 'SETOF ';
        }

        $sql .= "{$returns} AS ";

        if (is_array($definition)) {
            $this->arrayClean($definition);
            $sql .= "'" . $definition[0] . "'";
            if ($definition[1]) {
                $sql .= ",'" . $definition[1] . "'";
            }
        } else {
            $this->clean($definition);
            $sql .= "'" . $definition . "'";
        }

        $sql .= " LANGUAGE \"{$language}\"";

        // Add costs
        if (!empty($cost)) {
            $sql .= " COST {$cost}";
        }

        if ($rows != 0) {
            $sql .= " ROWS {$rows}";
        }

        // Add flags
        foreach ($flags as $v) {
            // Skip default flags
            if ($v == '') {
                continue;
            }

            $sql .= "\n{$v}";
        }

        $status = $this->execute($sql);
        if ($status != 0) {
            $this->rollbackTransaction();

            return -3;
        }

        /* set the comment */
        $status = $this->setComment('FUNCTION', "\"{$funcname}\"({$args})", null, $comment);
        if ($status != 0) {
            $this->rollbackTransaction();

            return -4;
        }

        return $this->endTransaction();
    }

    public function hasFunctionAlterOwner()
    {
        return true;
    }

    // Trigger functions

    public function hasFunctionAlterSchema()
    {
        return true;
    }

    /**
     * Drops a function.
     *
     * @param $function_oid The OID of the function to drop
     * @param $cascade      True to cascade drop, false to restrict
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function dropFunction($function_oid, $cascade)
    {
        // Function comes in with $object as function OID
        $fn       = $this->getFunction($function_oid);
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($fn->fields['proname']);

        $sql = "DROP FUNCTION \"{$f_schema}\".\"{$fn->fields['proname']}\"({$fn->fields['proarguments']})";
        if ($cascade) {
            $sql .= ' CASCADE';
        }

        return $this->execute($sql);
    }

    /**
     * Returns all details for a particular function
     *
     * @param $function_oid
     * @return \PHPPgAdmin\Database\Function info
     * @internal param \PHPPgAdmin\Database\The $func name of the function to retrieve
     */
    public function getFunction($function_oid)
    {
        $this->clean($function_oid);

        $sql = "
			SELECT
				pc.oid AS prooid, proname,
				pg_catalog.pg_get_userbyid(proowner) AS proowner,
				nspname as proschema, lanname as prolanguage, procost, prorows,
				pg_catalog.format_type(prorettype, NULL) as proresult, prosrc,
				probin, proretset, proisstrict, provolatile, prosecdef,
				pg_catalog.oidvectortypes(pc.proargtypes) AS proarguments,
				proargnames AS proargnames,
				pg_catalog.obj_description(pc.oid, 'pg_proc') AS procomment,
				proconfig,
				(select array_agg( (select typname from pg_type pt
					where pt.oid = p.oid) ) from unnest(proallargtypes) p)
				AS proallarguments,
				proargmodes
			FROM
				pg_catalog.pg_proc pc, pg_catalog.pg_language pl,
				pg_catalog.pg_namespace pn
			WHERE
				pc.oid = '{$function_oid}'::oid AND pc.prolang = pl.oid
				AND pc.pronamespace = pn.oid
			";

        return $this->selectSet($sql);
    }

    /**
     * Returns all details for a particular type
     *
     * @param $typname The name of the view to retrieve
     * @return Type info
     */
    public function getType($typname)
    {
        $this->clean($typname);

        $sql = "SELECT typtype, typbyval, typname, typinput AS typin, typoutput AS typout, typlen, typalign
			FROM pg_type WHERE typname='{$typname}'";

        return $this->selectSet($sql);
    }

    /**
     * Returns a list of all types in the database
     *
     * @param bool|\PHPPgAdmin\Database\If $all        If true, will find all available types, if false just those in search path
     * @param bool|\PHPPgAdmin\Database\If $tabletypes If true, will include table types
     * @param bool|\PHPPgAdmin\Database\If $domains    If true, will include domains
     * @return \PHPPgAdmin\Database\A recordet
     */
    public function getTypes($all = false, $tabletypes = false, $domains = false)
    {
        if ($all) {
            $where = '1 = 1';
        } else {
            $c_schema = $this->_schema;
            $this->clean($c_schema);
            $where = "n.nspname = '{$c_schema}'";
        }
        // Never show system table types
        $where2 = "AND c.relnamespace NOT IN (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname LIKE 'pg@_%' ESCAPE '@')";

        // Create type filter
        $tqry = "'c'";
        if ($tabletypes) {
            $tqry .= ", 'r', 'v'";
        }

        // Create domain filter
        if (!$domains) {
            $where .= " AND t.typtype != 'd'";
        }

        $sql = "SELECT
				t.typname AS basename,
				pg_catalog.format_type(t.oid, NULL) AS typname,
				pu.usename AS typowner,
				t.typtype,
				pg_catalog.obj_description(t.oid, 'pg_type') AS typcomment
			FROM (pg_catalog.pg_type t
				LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace)
				LEFT JOIN pg_catalog.pg_user pu ON t.typowner = pu.usesysid
			WHERE (t.typrelid = 0 OR (SELECT c.relkind IN ({$tqry}) FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid {$where2}))
			AND t.typname !~ '^_'
			AND {$where}
			ORDER BY typname
		";

        return $this->selectSet($sql);
    }

    /**
     * Creates a new type
     *
     * @param $typname
     * @param $typin
     * @param $typout
     * @param $typlen
     * @param $typdef
     * @param $typelem
     * @param $typdelim
     * @param $typbyval
     * @param $typalign
     * @param $typstorage
     * @return \PHPPgAdmin\Database\A 0 success
     * @internal param $ ...
     */
    public function createType(
        $typname,
        $typin,
        $typout,
        $typlen,
        $typdef,
        $typelem,
        $typdelim,
        $typbyval,
        $typalign,
        $typstorage
    ) {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($typname);
        $this->fieldClean($typin);
        $this->fieldClean($typout);

        $sql = "
			CREATE TYPE \"{$f_schema}\".\"{$typname}\" (
				INPUT = \"{$typin}\",
				OUTPUT = \"{$typout}\",
				INTERNALLENGTH = {$typlen}";
        if ($typdef != '') {
            $sql .= ", DEFAULT = {$typdef}";
        }

        if ($typelem != '') {
            $sql .= ", ELEMENT = {$typelem}";
        }

        if ($typdelim != '') {
            $sql .= ", DELIMITER = {$typdelim}";
        }

        if ($typbyval) {
            $sql .= ', PASSEDBYVALUE, ';
        }

        if ($typalign != '') {
            $sql .= ", ALIGNMENT = {$typalign}";
        }

        if ($typstorage != '') {
            $sql .= ", STORAGE = {$typstorage}";
        }

        $sql .= ')';

        return $this->execute($sql);
    }

    /**
     * Drops a type.
     *
     * @param $typname The name of the type to drop
     * @param $cascade True to cascade drop, false to restrict
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function dropType($typname, $cascade)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($typname);

        $sql = "DROP TYPE \"{$f_schema}\".\"{$typname}\"";
        if ($cascade) {
            $sql .= ' CASCADE';
        }

        return $this->execute($sql);
    }

    /**
     * Creates a new enum type in the database
     *
     * @param $name       The name of the type
     * @param $values     An array of values
     * @param $typcomment Type comment
     * @return bool|int 0 success
     */
    public function createEnumType($name, $values, $typcomment)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($name);

        if (empty($values)) {
            return -2;
        }

        $status = $this->beginTransaction();
        if ($status != 0) {
            return -1;
        }

        $values = array_unique($values);

        $nbval = count($values);

        for ($i = 0; $i < $nbval; $i++) {
            $this->clean($values[$i]);
        }

        $sql = "CREATE TYPE \"{$f_schema}\".\"{$name}\" AS ENUM ('";
        $sql .= implode("','", $values);
        $sql .= "')";

        $status = $this->execute($sql);
        if ($status) {
            $this->rollbackTransaction();

            return -1;
        }

        if ($typcomment != '') {
            $status = $this->setComment('TYPE', $name, '', $typcomment, true);
            if ($status) {
                $this->rollbackTransaction();

                return -1;
            }
        }

        return $this->endTransaction();
    }

    /**
     * Get defined values for a given enum
     *
     * @param $name
     * @return \PHPPgAdmin\Database\A recordset
     */
    public function getEnumValues($name)
    {
        $this->clean($name);

        $sql = "SELECT enumlabel AS enumval
		FROM pg_catalog.pg_type t JOIN pg_catalog.pg_enum e ON (t.oid=e.enumtypid)
		WHERE t.typname = '{$name}' ORDER BY e.oid";

        return $this->selectSet($sql);
    }

    // Operator functions

    /**
     * Creates a new composite type in the database
     *
     * @param $name       The name of the type
     * @param $fields     The number of fields
     * @param $field      An array of field names
     * @param $type       An array of field types
     * @param $array      An array of '' or '[]' for each type if it's an array or not
     * @param $length     An array of field lengths
     * @param $colcomment An array of comments
     * @param $typcomment Type comment
     * @return bool|int 0 success
     */
    public function createCompositeType($name, $fields, $field, $type, $array, $length, $colcomment, $typcomment)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($name);

        $status = $this->beginTransaction();
        if ($status != 0) {
            return -1;
        }

        $found       = false;
        $first       = true;
        $comment_sql = ''; // Accumulate comments for the columns
        $sql         = "CREATE TYPE \"{$f_schema}\".\"{$name}\" AS (";
        for ($i = 0; $i < $fields; $i++) {
            $this->fieldClean($field[$i]);
            $this->clean($type[$i]);
            $this->clean($length[$i]);
            $this->clean($colcomment[$i]);

            // Skip blank columns - for user convenience
            if ($field[$i] == '' || $type[$i] == '') {
                continue;
            }

            // If not the first column, add a comma
            if (!$first) {
                $sql .= ', ';
            } else {
                $first = false;
            }

            switch ($type[$i]) {
                // Have to account for weird placing of length for with/without
                // time zone types
                case 'timestamp with time zone':
                case 'timestamp without time zone':
                    $qual = substr($type[$i], 9);
                    $sql .= "\"{$field[$i]}\" timestamp";
                    if ($length[$i] != '') {
                        $sql .= "({$length[$i]})";
                    }

                    $sql .= $qual;
                    break;
                case 'time with time zone':
                case 'time without time zone':
                    $qual = substr($type[$i], 4);
                    $sql .= "\"{$field[$i]}\" time";
                    if ($length[$i] != '') {
                        $sql .= "({$length[$i]})";
                    }

                    $sql .= $qual;
                    break;
                default:
                    $sql .= "\"{$field[$i]}\" {$type[$i]}";
                    if ($length[$i] != '') {
                        $sql .= "({$length[$i]})";
                    }
            }
            // Add array qualifier if necessary
            if ($array[$i] == '[]') {
                $sql .= '[]';
            }

            if ($colcomment[$i] != '') {
                $comment_sql .= "COMMENT ON COLUMN \"{$f_schema}\".\"{$name}\".\"{$field[$i]}\" IS '{$colcomment[$i]}';\n";
            }

            $found = true;
        }

        if (!$found) {
            return -1;
        }

        $sql .= ')';

        $status = $this->execute($sql);
        if ($status) {
            $this->rollbackTransaction();

            return -1;
        }

        if ($typcomment != '') {
            $status = $this->setComment('TYPE', $name, '', $typcomment, true);
            if ($status) {
                $this->rollbackTransaction();

                return -1;
            }
        }

        if ($comment_sql != '') {
            $status = $this->execute($comment_sql);
            if ($status) {
                $this->rollbackTransaction();

                return -1;
            }
        }

        return $this->endTransaction();
    }

    /**
     * Returns a list of all casts in the database
     *
     * @return All casts
     */
    public function getCasts()
    {
        $conf = $this->conf;

        if ($conf['show_system']) {
            $where = '';
        } else {
            $where = '
				AND n1.nspname NOT LIKE $$pg\_%$$
				AND n2.nspname NOT LIKE $$pg\_%$$
				AND n3.nspname NOT LIKE $$pg\_%$$
			';
        }

        $sql = "
			SELECT
				c.castsource::pg_catalog.regtype AS castsource,
				c.casttarget::pg_catalog.regtype AS casttarget,
				CASE WHEN c.castfunc=0 THEN NULL
				ELSE c.castfunc::pg_catalog.regprocedure END AS castfunc,
				c.castcontext,
				obj_description(c.oid, 'pg_cast') as castcomment
			FROM
				(pg_catalog.pg_cast c LEFT JOIN pg_catalog.pg_proc p ON c.castfunc=p.oid JOIN pg_catalog.pg_namespace n3 ON p.pronamespace=n3.oid),
				pg_catalog.pg_type t1,
				pg_catalog.pg_type t2,
				pg_catalog.pg_namespace n1,
				pg_catalog.pg_namespace n2
			WHERE
				c.castsource=t1.oid
				AND c.casttarget=t2.oid
				AND t1.typnamespace=n1.oid
				AND t2.typnamespace=n2.oid
				{$where}
			ORDER BY 1, 2
		";

        return $this->selectSet($sql);
    }

    /**
     * Returns a list of all conversions in the database
     *
     * @return All conversions
     */
    public function getConversions()
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $sql = "
			SELECT
			       c.conname,
			       pg_catalog.pg_encoding_to_char(c.conforencoding) AS conforencoding,
			       pg_catalog.pg_encoding_to_char(c.contoencoding) AS contoencoding,
			       c.condefault,
			       pg_catalog.obj_description(c.oid, 'pg_conversion') AS concomment
			FROM pg_catalog.pg_conversion c, pg_catalog.pg_namespace n
			WHERE n.oid = c.connamespace
			      AND n.nspname='{$c_schema}'
			ORDER BY 1;
		";

        return $this->selectSet($sql);
    }

    // Operator Class functions

    /**
     * Edits a rule on a table OR view
     *
     * @param $name    The name of the new rule
     * @param $event   SELECT, INSERT, UPDATE or DELETE
     * @param $table   Table on which to create the rule
     * @param $where   When to execute the rule, '' indicates always
     * @param $instead True if an INSTEAD rule, false otherwise
     * @param $type    NOTHING for a do nothing rule, SOMETHING to use given action
     * @param $action  The action to take
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function setRule($name, $event, $table, $where, $instead, $type, $action)
    {
        return $this->createRule($name, $event, $table, $where, $instead, $type, $action, true);
    }

    // FTS functions

    /**
     * Creates a rule
     *
     * @param      $name    The name of the new rule
     * @param      $event   SELECT, INSERT, UPDATE or DELETE
     * @param      $table   Table on which to create the rule
     * @param      $where   When to execute the rule, '' indicates always
     * @param      $instead True if an INSTEAD rule, false otherwise
     * @param      $type    NOTHING for a do nothing rule, SOMETHING to use given action
     * @param      $action  The action to take
     * @param bool $replace (optional) True to replace existing rule, false otherwise
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function createRule($name, $event, $table, $where, $instead, $type, $action, $replace = false)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($name);
        $this->fieldClean($table);
        if (!in_array($event, $this->rule_events)) {
            return -1;
        }

        $sql = 'CREATE';
        if ($replace) {
            $sql .= ' OR REPLACE';
        }

        $sql .= " RULE \"{$name}\" AS ON {$event} TO \"{$f_schema}\".\"{$table}\"";
        // Can't escape WHERE clause
        if ($where != '') {
            $sql .= " WHERE {$where}";
        }

        $sql .= ' DO';
        if ($instead) {
            $sql .= ' INSTEAD';
        }

        if ($type == 'NOTHING') {
            $sql .= ' NOTHING';
        } else {
            $sql .= " ({$action})";
        }

        return $this->execute($sql);
    }

    /**
     * Removes a rule from a table OR view
     *
     * @param $rule     The rule to drop
     * @param $relation The relation from which to drop
     * @param $cascade  True to cascade drop, false to restrict
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function dropRule($rule, $relation, $cascade)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($rule);
        $this->fieldClean($relation);

        $sql = "DROP RULE \"{$rule}\" ON \"{$f_schema}\".\"{$relation}\"";
        if ($cascade) {
            $sql .= ' CASCADE';
        }

        return $this->execute($sql);
    }

    /**
     * Grabs a single trigger
     *
     * @param $table   The name of a table whose triggers to retrieve
     * @param $trigger The name of the trigger to retrieve
     * @return A recordset
     */
    public function getTrigger($table, $trigger)
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $this->clean($table);
        $this->clean($trigger);

        $sql = "
			SELECT * FROM pg_catalog.pg_trigger t, pg_catalog.pg_class c
			WHERE t.tgrelid=c.oid AND c.relname='{$table}' AND t.tgname='{$trigger}'
				AND c.relnamespace=(
					SELECT oid FROM pg_catalog.pg_namespace
					WHERE nspname='{$c_schema}')";

        return $this->selectSet($sql);
    }

    /**
     * A helper function for getTriggers that translates
     * an array of attribute numbers to an array of field names.
     * Note: Only needed for pre-7.4 servers, this function is deprecated
     *
     * @param $trigger An array containing fields from the trigger table
     * @return The trigger definition string
     */
    public function getTriggerDef($trigger)
    {

        $this->fieldArrayClean($trigger);
        // Constants to figure out tgtype
        if (!defined('TRIGGER_TYPE_ROW')) {
            define('TRIGGER_TYPE_ROW', 1 << 0);
        }

        if (!defined('TRIGGER_TYPE_BEFORE')) {
            define('TRIGGER_TYPE_BEFORE', 1 << 1);
        }

        if (!defined('TRIGGER_TYPE_INSERT')) {
            define('TRIGGER_TYPE_INSERT', 1 << 2);
        }

        if (!defined('TRIGGER_TYPE_DELETE')) {
            define('TRIGGER_TYPE_DELETE', 1 << 3);
        }

        if (!defined('TRIGGER_TYPE_UPDATE')) {
            define('TRIGGER_TYPE_UPDATE', 1 << 4);
        }

        $trigger['tgisconstraint'] = $this->phpBool($trigger['tgisconstraint']);
        $trigger['tgdeferrable']   = $this->phpBool($trigger['tgdeferrable']);
        $trigger['tginitdeferred'] = $this->phpBool($trigger['tginitdeferred']);

        // Constraint trigger or normal trigger
        if ($trigger['tgisconstraint']) {
            $tgdef = 'CREATE CONSTRAINT TRIGGER ';
        } else {
            $tgdef = 'CREATE TRIGGER ';
        }

        $tgdef .= "\"{$trigger['tgname']}\" ";

        // Trigger type
        $findx = 0;
        if (($trigger['tgtype'] & TRIGGER_TYPE_BEFORE) == TRIGGER_TYPE_BEFORE) {
            $tgdef .= 'BEFORE';
        } else {
            $tgdef .= 'AFTER';
        }

        if (($trigger['tgtype'] & TRIGGER_TYPE_INSERT) == TRIGGER_TYPE_INSERT) {
            $tgdef .= ' INSERT';
            $findx++;
        }
        if (($trigger['tgtype'] & TRIGGER_TYPE_DELETE) == TRIGGER_TYPE_DELETE) {
            if ($findx > 0) {
                $tgdef .= ' OR DELETE';
            } else {
                $tgdef .= ' DELETE';
                $findx++;
            }
        }
        if (($trigger['tgtype'] & TRIGGER_TYPE_UPDATE) == TRIGGER_TYPE_UPDATE) {
            if ($findx > 0) {
                $tgdef .= ' OR UPDATE';
            } else {
                $tgdef .= ' UPDATE';
            }
        }

        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        // Table name
        $tgdef .= " ON \"{$f_schema}\".\"{$trigger['relname']}\" ";

        // Deferrability
        if ($trigger['tgisconstraint']) {
            if ($trigger['tgconstrrelid'] != 0) {
                // Assume constrelname is not null
                $tgdef .= " FROM \"{$trigger['tgconstrrelname']}\" ";
            }
            if (!$trigger['tgdeferrable']) {
                $tgdef .= 'NOT ';
            }

            $tgdef .= 'DEFERRABLE INITIALLY ';
            if ($trigger['tginitdeferred']) {
                $tgdef .= 'DEFERRED ';
            } else {
                $tgdef .= 'IMMEDIATE ';
            }
        }

        // Row or statement
        if ($trigger['tgtype'] & TRIGGER_TYPE_ROW == TRIGGER_TYPE_ROW) {
            $tgdef .= 'FOR EACH ROW ';
        } else {
            $tgdef .= 'FOR EACH STATEMENT ';
        }

        // Execute procedure
        $tgdef .= "EXECUTE PROCEDURE \"{$trigger['tgfname']}\"(";

        // Parameters
        // Escape null characters
        $v = addcslashes($trigger['tgargs'], "\0");
        // Split on escaped null characters
        $params = explode('\\000', $v);
        for ($findx = 0; $findx < $trigger['tgnargs']; $findx++) {
            $param = "'" . str_replace('\'', '\\\'', $params[$findx]) . "'";
            $tgdef .= $param;
            if ($findx < ($trigger['tgnargs'] - 1)) {
                $tgdef .= ', ';
            }
        }

        // Finish it off
        $tgdef .= ')';

        return $tgdef;
    }

    /**
     * Returns a list of all functions that can be used in triggers
     */
    public function getTriggerFunctions()
    {
        return $this->getFunctions(true, 'trigger');
    }

    /**
     * Returns a list of all functions in the database
     *
     * @param bool|\PHPPgAdmin\Database\If $all  If true, will find all available functions, if false just those in search path
     * @param                              $type If not null, will find all functions with return value = type
     * @return \PHPPgAdmin\Database\All functions
     */
    public function getFunctions($all = false, $type = null)
    {
        if ($all) {
            $where    = 'pg_catalog.pg_function_is_visible(p.oid)';
            $distinct = 'DISTINCT ON (p.proname)';

            if ($type) {
                $where .= " AND p.prorettype = (select oid from pg_catalog.pg_type p where p.typname = 'trigger') ";
            }
        } else {
            $c_schema = $this->_schema;
            $this->clean($c_schema);
            $where    = "n.nspname = '{$c_schema}'";
            $distinct = '';
        }

        $sql = "
			SELECT
				{$distinct}
				p.oid AS prooid,
				p.proname,
				p.proretset,
				pg_catalog.format_type(p.prorettype, NULL) AS proresult,
				pg_catalog.oidvectortypes(p.proargtypes) AS proarguments,
				pl.lanname AS prolanguage,
				pg_catalog.obj_description(p.oid, 'pg_proc') AS procomment,
				p.proname || ' (' || pg_catalog.oidvectortypes(p.proargtypes) || ')' AS proproto,
				CASE WHEN p.proretset THEN 'setof ' ELSE '' END || pg_catalog.format_type(p.prorettype, NULL) AS proreturns,
				u.usename AS proowner
			FROM pg_catalog.pg_proc p
				INNER JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
				INNER JOIN pg_catalog.pg_language pl ON pl.oid = p.prolang
				LEFT JOIN pg_catalog.pg_user u ON u.usesysid = p.proowner
			WHERE NOT p.proisagg
				AND {$where}
			ORDER BY p.proname, proresult
			";

        return $this->selectSet($sql);
    }

    /**
     * Creates a trigger
     *
     * @param $tgname  The name of the trigger to create
     * @param $table   The name of the table
     * @param $tgproc  The function to execute
     * @param $tgtime  BEFORE or AFTER
     * @param $tgevent Event
     * @param $tgfrequency
     * @param $tgargs  The function arguments
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function createTrigger($tgname, $table, $tgproc, $tgtime, $tgevent, $tgfrequency, $tgargs)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($tgname);
        $this->fieldClean($table);
        $this->fieldClean($tgproc);

        /* No Statement Level Triggers in PostgreSQL (by now) */
        $sql = "CREATE TRIGGER \"{$tgname}\" {$tgtime}
				{$tgevent} ON \"{$f_schema}\".\"{$table}\"
				FOR EACH {$tgfrequency} EXECUTE PROCEDURE \"{$tgproc}\"({$tgargs})";

        return $this->execute($sql);
    }

    /**
     * Alters a trigger
     *
     * @param $table   The name of the table containing the trigger
     * @param $trigger The name of the trigger to alter
     * @param $name    The new name for the trigger
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function alterTrigger($table, $trigger, $name)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($table);
        $this->fieldClean($trigger);
        $this->fieldClean($name);

        $sql = "ALTER TRIGGER \"{$trigger}\" ON \"{$f_schema}\".\"{$table}\" RENAME TO \"{$name}\"";

        return $this->execute($sql);
    }

    /**
     * Drops a trigger
     *
     * @param $tgname  The name of the trigger to drop
     * @param $table   The table from which to drop the trigger
     * @param $cascade True to cascade drop, false to restrict
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function dropTrigger($tgname, $table, $cascade)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($tgname);
        $this->fieldClean($table);

        $sql = "DROP TRIGGER \"{$tgname}\" ON \"{$f_schema}\".\"{$table}\"";
        if ($cascade) {
            $sql .= ' CASCADE';
        }

        return $this->execute($sql);
    }

    /**
     * Enables a trigger
     *
     * @param $tgname The name of the trigger to enable
     * @param $table  The table in which to enable the trigger
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function enableTrigger($tgname, $table)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($tgname);
        $this->fieldClean($table);

        $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" ENABLE TRIGGER \"{$tgname}\"";

        return $this->execute($sql);
    }

    /**
     * Disables a trigger
     *
     * @param $tgname The name of the trigger to disable
     * @param $table  The table in which to disable the trigger
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function disableTrigger($tgname, $table)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($tgname);
        $this->fieldClean($table);

        $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" DISABLE TRIGGER \"{$tgname}\"";

        return $this->execute($sql);
    }

    /**
     * Returns a list of all operators in the database
     *
     * @return All operators
     */
    public function getOperators()
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        // We stick with the subselects here, as you cannot ORDER BY a regtype
        $sql = "
			SELECT
            	po.oid,	po.oprname,
				(SELECT pg_catalog.format_type(oid, NULL) FROM pg_catalog.pg_type pt WHERE pt.oid=po.oprleft) AS oprleftname,
				(SELECT pg_catalog.format_type(oid, NULL) FROM pg_catalog.pg_type pt WHERE pt.oid=po.oprright) AS oprrightname,
				po.oprresult::pg_catalog.regtype AS resultname,
		        pg_catalog.obj_description(po.oid, 'pg_operator') AS oprcomment
			FROM
				pg_catalog.pg_operator po
			WHERE
				po.oprnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname='{$c_schema}')
			ORDER BY
				po.oprname, oprleftname, oprrightname
		";

        return $this->selectSet($sql);
    }

    /**
     * Drops an operator
     *
     * @param $operator_oid The OID of the operator to drop
     * @param $cascade      True to cascade drop, false to restrict
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function dropOperator($operator_oid, $cascade)
    {
        // Function comes in with $object as operator OID
        $opr      = $this->getOperator($operator_oid);
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($opr->fields['oprname']);

        $sql = "DROP OPERATOR \"{$f_schema}\".{$opr->fields['oprname']} (";
        // Quoting or formatting here???
        if ($opr->fields['oprleftname'] !== null) {
            $sql .= $opr->fields['oprleftname'] . ', ';
        } else {
            $sql .= 'NONE, ';
        }

        if ($opr->fields['oprrightname'] !== null) {
            $sql .= $opr->fields['oprrightname'] . ')';
        } else {
            $sql .= 'NONE)';
        }

        if ($cascade) {
            $sql .= ' CASCADE';
        }

        return $this->execute($sql);
    }

    /**
     * Returns all details for a particular operator
     *
     * @param $operator_oid The oid of the operator
     * @return Function info
     */
    public function getOperator($operator_oid)
    {
        $this->clean($operator_oid);

        $sql = "
			SELECT
            	po.oid, po.oprname,
				oprleft::pg_catalog.regtype AS oprleftname,
				oprright::pg_catalog.regtype AS oprrightname,
				oprresult::pg_catalog.regtype AS resultname,
				po.oprcanhash,
				oprcanmerge,
				oprcom::pg_catalog.regoperator AS oprcom,
				oprnegate::pg_catalog.regoperator AS oprnegate,
				po.oprcode::pg_catalog.regproc AS oprcode,
				po.oprrest::pg_catalog.regproc AS oprrest,
				po.oprjoin::pg_catalog.regproc AS oprjoin
			FROM
				pg_catalog.pg_operator po
			WHERE
				po.oid='{$operator_oid}'
		";

        return $this->selectSet($sql);
    }

    /**
     *  Gets all opclasses
     *
     * @return A recordset
     */

    public function getOpClasses()
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $sql = "
			SELECT
				pa.amname, po.opcname,
				po.opcintype::pg_catalog.regtype AS opcintype,
				po.opcdefault,
				pg_catalog.obj_description(po.oid, 'pg_opclass') AS opccomment
			FROM
				pg_catalog.pg_opclass po, pg_catalog.pg_am pa, pg_catalog.pg_namespace pn
			WHERE
				po.opcmethod=pa.oid
				AND po.opcnamespace=pn.oid
				AND pn.nspname='{$c_schema}'
			ORDER BY 1,2
			";

        return $this->selectSet($sql);
    }

    /**
     * Creates a new FTS configuration.
     *
     * @param string $cfgname  The name of the FTS configuration to create
     * @param string $parser   The parser to be used in new FTS configuration
     * @param string $template The existing FTS configuration to be used as template for the new one
     * @param string $comment  If omitted, defaults to nothing
     * @return bool|int 0 success
     * @internal param string $locale Locale of the FTS configuration
     * @internal param string $withmap Should we copy whole map of existing FTS configuration to the new one
     * @internal param string $makeDefault Should this configuration be the default for locale given
     */
    public function createFtsConfiguration($cfgname, $parser = '', $template = '', $comment = '')
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($cfgname);

        $sql = "CREATE TEXT SEARCH CONFIGURATION \"{$f_schema}\".\"{$cfgname}\" (";
        if ($parser != '') {
            $this->fieldClean($parser['schema']);
            $this->fieldClean($parser['parser']);
            $parser = "\"{$parser['schema']}\".\"{$parser['parser']}\"";
            $sql .= " PARSER = {$parser}";
        }
        if ($template != '') {
            $this->fieldClean($template['schema']);
            $this->fieldClean($template['name']);
            $sql .= " COPY = \"{$template['schema']}\".\"{$template['name']}\"";
        }
        $sql .= ')';

        if ($comment != '') {
            $status = $this->beginTransaction();
            if ($status != 0) {
                return -1;
            }
        }

        // Create the FTS configuration
        $status = $this->execute($sql);
        if ($status != 0) {
            $this->rollbackTransaction();

            return -1;
        }

        // Set the comment
        if ($comment != '') {
            $status = $this->setComment('TEXT SEARCH CONFIGURATION', $cfgname, '', $comment);
            if ($status != 0) {
                $this->rollbackTransaction();

                return -1;
            }

            return $this->endTransaction();
        }

        return 0;
    }

    // Language functions

    /**
     * Returns available FTS configurations
     *
     * @param bool|\PHPPgAdmin\Database\if $all if false, returns schema qualified FTS confs
     * @return \PHPPgAdmin\Database\A recordset
     */
    public function getFtsConfigurations($all = true)
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $sql = "
			SELECT
				n.nspname as schema,
				c.cfgname as name,
				pg_catalog.obj_description(c.oid, 'pg_ts_config') as comment
			FROM
				pg_catalog.pg_ts_config c
				JOIN pg_catalog.pg_namespace n ON n.oid = c.cfgnamespace
			WHERE
				pg_catalog.pg_ts_config_is_visible(c.oid)";

        if (!$all) {
            $sql .= " AND  n.nspname='{$c_schema}'\n";
        }

        $sql .= 'ORDER BY name';

        return $this->selectSet($sql);
    }

    // Aggregate functions

    /**
     * Returns the map of FTS configuration given
     * (list of mappings (tokens) and their processing dictionaries)
     *
     * @param string $ftscfg Name of the FTS configuration
     *
     * @return RecordSet
     */
    public function getFtsConfigurationMap($ftscfg)
    {

        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $this->fieldClean($ftscfg);

        $oidSet = $this->selectSet("SELECT c.oid
			FROM pg_catalog.pg_ts_config AS c
				LEFT JOIN pg_catalog.pg_namespace n ON (n.oid = c.cfgnamespace)
			WHERE c.cfgname = '{$ftscfg}'
				AND n.nspname='{$c_schema}'");

        $oid = $oidSet->fields['oid'];

        $sql = "
 			SELECT
    			(SELECT t.alias FROM pg_catalog.ts_token_type(c.cfgparser) AS t WHERE t.tokid = m.maptokentype) AS name,
        		(SELECT t.description FROM pg_catalog.ts_token_type(c.cfgparser) AS t WHERE t.tokid = m.maptokentype) AS description,
				c.cfgname AS cfgname, n.nspname ||'.'|| d.dictname as dictionaries
			FROM
				pg_catalog.pg_ts_config AS c, pg_catalog.pg_ts_config_map AS m, pg_catalog.pg_ts_dict d,
				pg_catalog.pg_namespace n
			WHERE
				c.oid = {$oid}
				AND m.mapcfg = c.oid
				AND m.mapdict = d.oid
				AND d.dictnamespace = n.oid
			ORDER BY name
			";

        return $this->selectSet($sql);
    }

    /**
     * Returns FTS parsers available
     *
     * @param bool|\PHPPgAdmin\Database\if $all if false, return only Parsers from the current schema
     * @return \PHPPgAdmin\Database\RecordSet
     */
    public function getFtsParsers($all = true)
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $sql = "
			SELECT
			   n.nspname as schema,
			   p.prsname as name,
			   pg_catalog.obj_description(p.oid, 'pg_ts_parser') as comment
			FROM pg_catalog.pg_ts_parser p
				LEFT JOIN pg_catalog.pg_namespace n ON (n.oid = p.prsnamespace)
			WHERE pg_catalog.pg_ts_parser_is_visible(p.oid)";

        if (!$all) {
            $sql .= " AND n.nspname='{$c_schema}'\n";
        }

        $sql .= 'ORDER BY name';

        return $this->selectSet($sql);
    }

    /**
     * Returns FTS dictionaries available
     *
     * @param bool|\PHPPgAdmin\Database\if $all if false, return only Dics from the current schema
     * @return \PHPPgAdmin\Database\RecordSet
     */
    public function getFtsDictionaries($all = true)
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $sql = "
 			SELECT
				n.nspname as schema, d.dictname as name,
				pg_catalog.obj_description(d.oid, 'pg_ts_dict') as comment
			FROM pg_catalog.pg_ts_dict d
				LEFT JOIN pg_catalog.pg_namespace n ON n.oid = d.dictnamespace
			WHERE pg_catalog.pg_ts_dict_is_visible(d.oid)";

        if (!$all) {
            $sql .= " AND n.nspname='{$c_schema}'\n";
        }

        $sql .= 'ORDER BY name;';

        return $this->selectSet($sql);
    }

    /**
     * Returns all FTS dictionary templates available
     */
    public function getFtsDictionaryTemplates()
    {

        $sql = "
 			SELECT
				n.nspname as schema,
				t.tmplname as name,
				( SELECT COALESCE(np.nspname, '(null)')::pg_catalog.text || '.' || p.proname
					FROM pg_catalog.pg_proc p
					LEFT JOIN pg_catalog.pg_namespace np ON np.oid = p.pronamespace
					WHERE t.tmplinit = p.oid ) AS  init,
				( SELECT COALESCE(np.nspname, '(null)')::pg_catalog.text || '.' || p.proname
					FROM pg_catalog.pg_proc p
					LEFT JOIN pg_catalog.pg_namespace np ON np.oid = p.pronamespace
					WHERE t.tmpllexize = p.oid ) AS  lexize,
				pg_catalog.obj_description(t.oid, 'pg_ts_template') as comment
			FROM pg_catalog.pg_ts_template t
				LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.tmplnamespace
			WHERE pg_catalog.pg_ts_template_is_visible(t.oid)
			ORDER BY name;";

        return $this->selectSet($sql);
    }

    /**
     * Drops FTS coniguration
     *
     * @param $ftscfg  The configuration's name
     * @param $cascade Cascade to dependenced objects
     * @return \PHPPgAdmin\Database\A 0 on success
     */
    public function dropFtsConfiguration($ftscfg, $cascade)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($ftscfg);

        $sql = "DROP TEXT SEARCH CONFIGURATION \"{$f_schema}\".\"{$ftscfg}\"";
        if ($cascade) {
            $sql .= ' CASCADE';
        }

        return $this->execute($sql);
    }

    /**
     * Drops FTS dictionary
     *
     * @param $ftsdict The dico's name
     * @param $cascade Cascade to dependenced objects
     * @return \PHPPgAdmin\Database\A 0 on success
     * @todo Support of dictionary templates dropping
     */
    public function dropFtsDictionary($ftsdict, $cascade)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($ftsdict);

        $sql = 'DROP TEXT SEARCH DICTIONARY';
        $sql .= " \"{$f_schema}\".\"{$ftsdict}\"";
        if ($cascade) {
            $sql .= ' CASCADE';
        }

        return $this->execute($sql);
    }

    /**
     * Alters FTS configuration
     *
     * @param $cfgname The conf's name
     * @param $comment A comment on for the conf
     * @param $name    The new conf name
     * @return bool|int 0 on success
     */
    public function updateFtsConfiguration($cfgname, $comment, $name)
    {

        $status = $this->beginTransaction();
        if ($status != 0) {
            $this->rollbackTransaction();

            return -1;
        }

        $this->fieldClean($cfgname);

        $status = $this->setComment('TEXT SEARCH CONFIGURATION', $cfgname, '', $comment);
        if ($status != 0) {
            $this->rollbackTransaction();

            return -1;
        }

        // Only if the name has changed
        if ($name != $cfgname) {
            $f_schema = $this->_schema;
            $this->fieldClean($f_schema);
            $this->fieldClean($name);

            $sql    = "ALTER TEXT SEARCH CONFIGURATION \"{$f_schema}\".\"{$cfgname}\" RENAME TO \"{$name}\"";
            $status = $this->execute($sql);
            if ($status != 0) {
                $this->rollbackTransaction();

                return -1;
            }
        }

        return $this->endTransaction();
    }

    /**
     * Creates a new FTS dictionary or FTS dictionary template.
     *
     * @param string  $dictname   The name of the FTS dictionary to create
     * @param boolean $isTemplate Flag whether we create usual dictionary or dictionary template
     * @param string  $template   The existing FTS dictionary to be used as template for the new one
     * @param string  $lexize     The name of the function, which does transformation of input word
     * @param string  $init       The name of the function, which initializes dictionary
     * @param string  $option     Usually, it stores various options required for the dictionary
     * @param string  $comment    If omitted, defaults to nothing
     * @return bool|int 0 success
     */
    public function createFtsDictionary(
        $dictname,
        $isTemplate = false,
        $template = '',
        $lexize = '',
        $init = '',
        $option = '',
        $comment = ''
    ) {

        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($dictname);
        $this->fieldClean($template);
        $this->fieldClean($lexize);
        $this->fieldClean($init);
        $this->fieldClean($option);

        $sql = 'CREATE TEXT SEARCH';
        if ($isTemplate) {
            $sql .= " TEMPLATE \"{$f_schema}\".\"{$dictname}\" (";
            if ($lexize != '') {
                $sql .= " LEXIZE = {$lexize}";
            }

            if ($init != '') {
                $sql .= ", INIT = {$init}";
            }

            $sql .= ')';
            $whatToComment = 'TEXT SEARCH TEMPLATE';
        } else {
            $sql .= " DICTIONARY \"{$f_schema}\".\"{$dictname}\" (";
            if ($template != '') {
                $this->fieldClean($template['schema']);
                $this->fieldClean($template['name']);
                $template = "\"{$template['schema']}\".\"{$template['name']}\"";

                $sql .= " TEMPLATE = {$template}";
            }
            if ($option != '') {
                $sql .= ", {$option}";
            }

            $sql .= ')';
            $whatToComment = 'TEXT SEARCH DICTIONARY';
        }

        /* if comment, begin a transaction to
         * run both commands */
        if ($comment != '') {
            $status = $this->beginTransaction();
            if ($status != 0) {
                return -1;
            }
        }

        // Create the FTS dictionary
        $status = $this->execute($sql);
        if ($status != 0) {
            $this->rollbackTransaction();

            return -1;
        }

        // Set the comment
        if ($comment != '') {
            $status = $this->setComment($whatToComment, $dictname, '', $comment);
            if ($status != 0) {
                $this->rollbackTransaction();

                return -1;
            }
        }

        return $this->endTransaction();
    }

    // Role, User/Group functions

    /**
     * Alters FTS dictionary or dictionary template
     *
     * @param $dictname The dico's name
     * @param $comment  The comment
     * @param $name     The new dico's name
     * @return bool|int 0 on success
     */
    public function updateFtsDictionary($dictname, $comment, $name)
    {

        $status = $this->beginTransaction();
        if ($status != 0) {
            $this->rollbackTransaction();

            return -1;
        }

        $this->fieldClean($dictname);
        $status = $this->setComment('TEXT SEARCH DICTIONARY', $dictname, '', $comment);
        if ($status != 0) {
            $this->rollbackTransaction();

            return -1;
        }

        // Only if the name has changed
        if ($name != $dictname) {
            $f_schema = $this->_schema;
            $this->fieldClean($f_schema);
            $this->fieldClean($name);

            $sql    = "ALTER TEXT SEARCH DICTIONARY \"{$f_schema}\".\"{$dictname}\" RENAME TO \"{$name}\"";
            $status = $this->execute($sql);
            if ($status != 0) {
                $this->rollbackTransaction();

                return -1;
            }
        }

        return $this->endTransaction();
    }

    /**
     * Return all information relating to a FTS dictionary
     *
     * @param $ftsdict The name of the FTS dictionary
     *
     * @return RecordSet of FTS dictionary information
     */
    public function getFtsDictionaryByName($ftsdict)
    {

        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $this->clean($ftsdict);

        $sql = "SELECT
			   n.nspname as schema,
			   d.dictname as name,
			   ( SELECT COALESCE(nt.nspname, '(null)')::pg_catalog.text || '.' || t.tmplname FROM
				 pg_catalog.pg_ts_template t
									  LEFT JOIN pg_catalog.pg_namespace nt ON nt.oid = t.tmplnamespace
									  WHERE d.dicttemplate = t.oid ) AS  template,
			   d.dictinitoption as init,
			   pg_catalog.obj_description(d.oid, 'pg_ts_dict') as comment
			FROM pg_catalog.pg_ts_dict d
				LEFT JOIN pg_catalog.pg_namespace n ON n.oid = d.dictnamespace
			WHERE d.dictname = '{$ftsdict}'
			   AND pg_catalog.pg_ts_dict_is_visible(d.oid)
			   AND n.nspname='{$c_schema}'
			ORDER BY name";

        return $this->selectSet($sql);
    }

    /**
     * Creates/updates/deletes FTS mapping.
     *
     * @param        $ftscfg
     * @param array  $mapping  Array of tokens' names
     * @param string $action   What to do with the mapping: add, alter or drop
     * @param string $dictname Dictionary that will process tokens given or null in case of drop action
     * @return int|\PHPPgAdmin\Database\A 0 success
     * @internal param string $cfgname The name of the FTS configuration to alter
     */
    public function changeFtsMapping($ftscfg, $mapping, $action, $dictname = null)
    {

        if (count($mapping) > 0) {
            $f_schema = $this->_schema;
            $this->fieldClean($f_schema);
            $this->fieldClean($ftscfg);
            $this->fieldClean($dictname);
            $this->arrayClean($mapping);

            switch ($action) {
                case 'alter':
                    $whatToDo = 'ALTER';
                    break;
                case 'drop':
                    $whatToDo = 'DROP';
                    break;
                default:
                    $whatToDo = 'ADD';
                    break;
            }

            $sql = "ALTER TEXT SEARCH CONFIGURATION \"{$f_schema}\".\"{$ftscfg}\" {$whatToDo} MAPPING FOR ";
            $sql .= implode(',', $mapping);
            if ($action != 'drop' && !empty($dictname)) {
                $sql .= " WITH {$dictname}";
            }

            return $this->execute($sql);
        }

        return -1;
    }

    /**
     * Return all information related to a given FTS configuration's mapping
     *
     * @param $ftscfg  The name of the FTS configuration
     * @param $mapping The name of the mapping
     *
     * @return FTS configuration information
     */
    public function getFtsMappingByName($ftscfg, $mapping)
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $this->clean($ftscfg);
        $this->clean($mapping);

        $oidSet = $this->selectSet("SELECT c.oid, cfgparser
			FROM pg_catalog.pg_ts_config AS c
				LEFT JOIN pg_catalog.pg_namespace AS n ON n.oid = c.cfgnamespace
			WHERE c.cfgname = '{$ftscfg}'
				AND n.nspname='{$c_schema}'");

        $oid       = $oidSet->fields['oid'];
        $cfgparser = $oidSet->fields['cfgparser'];

        $tokenIdSet = $this->selectSet("SELECT tokid
			FROM pg_catalog.ts_token_type({$cfgparser})
			WHERE alias = '{$mapping}'");

        $tokid = $tokenIdSet->fields['tokid'];

        $sql = "SELECT
			    (SELECT t.alias FROM pg_catalog.ts_token_type(c.cfgparser) AS t WHERE t.tokid = m.maptokentype) AS name,
    	            d.dictname as dictionaries
			FROM pg_catalog.pg_ts_config AS c, pg_catalog.pg_ts_config_map AS m, pg_catalog.pg_ts_dict d
			WHERE c.oid = {$oid} AND m.mapcfg = c.oid AND m.maptokentype = {$tokid} AND m.mapdict = d.oid
			LIMIT 1;";

        return $this->selectSet($sql);
    }

    /**
     * Return list of FTS mappings possible for given parser
     * (specified by given configuration since configuration can only have 1 parser)
     *
     * @param $ftscfg The config's name that use the parser
     * @return \PHPPgAdmin\Database\A 0 on success
     */
    public function getFtsMappings($ftscfg)
    {

        $cfg = $this->getFtsConfigurationByName($ftscfg);

        $sql = "SELECT alias AS name, description
			FROM pg_catalog.ts_token_type({$cfg->fields['parser_id']})
			ORDER BY name";

        return $this->selectSet($sql);
    }

    /**
     * Return all information related to a FTS configuration
     *
     * @param $ftscfg The name of the FTS configuration
     *
     * @return FTS configuration information
     */
    public function getFtsConfigurationByName($ftscfg)
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $this->clean($ftscfg);
        $sql = "
			SELECT
				n.nspname as schema,
				c.cfgname as name,
				p.prsname as parser,
				c.cfgparser as parser_id,
				pg_catalog.obj_description(c.oid, 'pg_ts_config') as comment
			FROM pg_catalog.pg_ts_config c
				LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.cfgnamespace
				LEFT JOIN pg_catalog.pg_ts_parser p ON p.oid = c.cfgparser
			WHERE pg_catalog.pg_ts_config_is_visible(c.oid)
				AND c.cfgname = '{$ftscfg}'
				AND n.nspname='{$c_schema}'";

        return $this->selectSet($sql);
    }

    /**
     * Gets all languages
     *
     * @param bool|True $all True to get all languages, regardless of show_system
     * @return \PHPPgAdmin\Database\A recordset
     */
    public function getLanguages($all = false)
    {
        $conf = $this->conf;

        if ($conf['show_system'] || $all) {
            $where = '';
        } else {
            $where = 'WHERE lanispl';
        }

        $sql = "
			SELECT
				lanname, lanpltrusted,
				lanplcallfoid::pg_catalog.regproc AS lanplcallf
			FROM
				pg_catalog.pg_language
			{$where}
			ORDER BY lanname
		";

        return $this->selectSet($sql);
    }

    /**
     * Creates a new aggregate in the database
     *
     * @param $name     The name of the aggregate
     * @param $basetype The input data type of the aggregate
     * @param $sfunc    The name of the state transition function for the aggregate
     * @param $stype    The data type for the aggregate's state value
     * @param $ffunc    The name of the final function for the aggregate
     * @param $initcond The initial setting for the state value
     * @param $sortop   The sort operator for the aggregate
     * @param $comment  Aggregate comment
     * @return bool|int 0 success
     */
    public function createAggregate($name, $basetype, $sfunc, $stype, $ffunc, $initcond, $sortop, $comment)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($name);
        $this->fieldClean($basetype);
        $this->fieldClean($sfunc);
        $this->fieldClean($stype);
        $this->fieldClean($ffunc);
        $this->fieldClean($initcond);
        $this->fieldClean($sortop);

        $this->beginTransaction();

        $sql = "CREATE AGGREGATE \"{$f_schema}\".\"{$name}\" (BASETYPE = \"{$basetype}\", SFUNC = \"{$sfunc}\", STYPE = \"{$stype}\"";
        if (trim($ffunc) != '') {
            $sql .= ", FINALFUNC = \"{$ffunc}\"";
        }

        if (trim($initcond) != '') {
            $sql .= ", INITCOND = \"{$initcond}\"";
        }

        if (trim($sortop) != '') {
            $sql .= ", SORTOP = \"{$sortop}\"";
        }

        $sql .= ')';

        $status = $this->execute($sql);
        if ($status) {
            $this->rollbackTransaction();

            return -1;
        }

        if (trim($comment) != '') {
            $status = $this->setComment('AGGREGATE', $name, '', $comment, $basetype);
            if ($status) {
                $this->rollbackTransaction();

                return -1;
            }
        }

        return $this->endTransaction();
    }

    /**
     * Removes an aggregate function from the database
     *
     * @param $aggrname The name of the aggregate
     * @param $aggrtype The input data type of the aggregate
     * @param $cascade  True to cascade drop, false to restrict
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function dropAggregate($aggrname, $aggrtype, $cascade)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($aggrname);
        $this->fieldClean($aggrtype);

        $sql = "DROP AGGREGATE \"{$f_schema}\".\"{$aggrname}\" (\"{$aggrtype}\")";
        if ($cascade) {
            $sql .= ' CASCADE';
        }

        return $this->execute($sql);
    }

    /**
     * Gets all information for an aggregate
     *
     * @param $name     The name of the aggregate
     * @param $basetype The input data type of the aggregate
     * @return A recordset
     */
    public function getAggregate($name, $basetype)
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $this->fieldClean($name);
        $this->fieldClean($basetype);

        $sql = "
			SELECT p.proname, CASE p.proargtypes[0]
				WHEN 'pg_catalog.\"any\"'::pg_catalog.regtype THEN NULL
				ELSE pg_catalog.format_type(p.proargtypes[0], NULL) END AS proargtypes,
				a.aggtransfn, format_type(a.aggtranstype, NULL) AS aggstype, a.aggfinalfn,
				a.agginitval, a.aggsortop, u.usename, pg_catalog.obj_description(p.oid, 'pg_proc') AS aggrcomment
			FROM pg_catalog.pg_proc p, pg_catalog.pg_namespace n, pg_catalog.pg_user u, pg_catalog.pg_aggregate a
			WHERE n.oid = p.pronamespace AND p.proowner=u.usesysid AND p.oid=a.aggfnoid
				AND p.proisagg AND n.nspname='{$c_schema}'
				AND p.proname='" . $name . "'
				AND CASE p.proargtypes[0]
					WHEN 'pg_catalog.\"any\"'::pg_catalog.regtype THEN ''
					ELSE pg_catalog.format_type(p.proargtypes[0], NULL)
				END ='" . $basetype . "'";

        return $this->selectSet($sql);
    }

    /**
     * Gets all aggregates
     *
     * @return A recordset
     */
    public function getAggregates()
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $sql = "SELECT p.proname, CASE p.proargtypes[0] WHEN 'pg_catalog.\"any\"'::pg_catalog.regtype THEN NULL ELSE
			   pg_catalog.format_type(p.proargtypes[0], NULL) END AS proargtypes, a.aggtransfn, u.usename,
			   pg_catalog.obj_description(p.oid, 'pg_proc') AS aggrcomment
			   FROM pg_catalog.pg_proc p, pg_catalog.pg_namespace n, pg_catalog.pg_user u, pg_catalog.pg_aggregate a
			   WHERE n.oid = p.pronamespace AND p.proowner=u.usesysid AND p.oid=a.aggfnoid
			   AND p.proisagg AND n.nspname='{$c_schema}' ORDER BY 1, 2";

        return $this->selectSet($sql);
    }

    /**
     * Alters an aggregate
     *
     * @param $aggrname       The actual name of the aggregate
     * @param $aggrtype       The actual input data type of the aggregate
     * @param $aggrowner      The actual owner of the aggregate
     * @param $aggrschema     The actual schema the aggregate belongs to
     * @param $aggrcomment    The actual comment for the aggregate
     * @param $newaggrname    The new name of the aggregate
     * @param $newaggrowner   The new owner of the aggregate
     * @param $newaggrschema  The new schema where the aggregate will belong to
     * @param $newaggrcomment The new comment for the aggregate
     * @return bool|int 0 success
     */
    public function alterAggregate(
        $aggrname,
        $aggrtype,
        $aggrowner,
        $aggrschema,
        $aggrcomment,
        $newaggrname,
        $newaggrowner,
        $newaggrschema,
        $newaggrcomment
    ) {
        // Clean fields
        $this->fieldClean($aggrname);
        $this->fieldClean($aggrtype);
        $this->fieldClean($aggrowner);
        $this->fieldClean($aggrschema);
        $this->fieldClean($newaggrname);
        $this->fieldClean($newaggrowner);
        $this->fieldClean($newaggrschema);

        $this->beginTransaction();

        // Change the owner, if it has changed
        if ($aggrowner != $newaggrowner) {
            $status = $this->changeAggregateOwner($aggrname, $aggrtype, $newaggrowner);
            if ($status != 0) {
                $this->rollbackTransaction();

                return -1;
            }
        }

        // Set the comment, if it has changed
        if ($aggrcomment != $newaggrcomment) {
            $status = $this->setComment('AGGREGATE', $aggrname, '', $newaggrcomment, $aggrtype);
            if ($status) {
                $this->rollbackTransaction();

                return -2;
            }
        }

        // Change the schema, if it has changed
        if ($aggrschema != $newaggrschema) {
            $status = $this->changeAggregateSchema($aggrname, $aggrtype, $newaggrschema);
            if ($status != 0) {
                $this->rollbackTransaction();

                return -3;
            }
        }

        // Rename the aggregate, if it has changed
        if ($aggrname != $newaggrname) {
            $status = $this->renameAggregate($newaggrschema, $aggrname, $aggrtype, $newaggrname);
            if ($status != 0) {
                $this->rollbackTransaction();

                return -4;
            }
        }

        return $this->endTransaction();
    }

    /**
     * Changes the owner of an aggregate function
     *
     * @param $aggrname     The name of the aggregate
     * @param $aggrtype     The input data type of the aggregate
     * @param $newaggrowner The new owner of the aggregate
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function changeAggregateOwner($aggrname, $aggrtype, $newaggrowner)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($aggrname);
        $this->fieldClean($newaggrowner);
        $sql = "ALTER AGGREGATE \"{$f_schema}\".\"{$aggrname}\" (\"{$aggrtype}\") OWNER TO \"{$newaggrowner}\"";

        return $this->execute($sql);
    }

    /**
     * Changes the schema of an aggregate function
     *
     * @param $aggrname      The name of the aggregate
     * @param $aggrtype      The input data type of the aggregate
     * @param $newaggrschema The new schema for the aggregate
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function changeAggregateSchema($aggrname, $aggrtype, $newaggrschema)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($aggrname);
        $this->fieldClean($newaggrschema);
        $sql = "ALTER AGGREGATE \"{$f_schema}\".\"{$aggrname}\" (\"{$aggrtype}\") SET SCHEMA  \"{$newaggrschema}\"";

        return $this->execute($sql);
    }

    /**
     * Renames an aggregate function
     *
     * @param $aggrschema
     * @param $aggrname    The actual name of the aggregate
     * @param $aggrtype    The actual input data type of the aggregate
     * @param $newaggrname The new name of the aggregate
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function renameAggregate($aggrschema, $aggrname, $aggrtype, $newaggrname)
    {
        /* this function is called from alterAggregate where params are cleaned */
        $sql = "ALTER AGGREGATE \"{$aggrschema}\"" . '.' . "\"{$aggrname}\" (\"{$aggrtype}\") RENAME TO \"{$newaggrname}\"";

        return $this->execute($sql);
    }

    /**
     * Returns all roles in the database cluster
     *
     * @param $rolename (optional) The role name to exclude from the select
     * @return All roles
     */
    public function getRoles($rolename = '')
    {
        $sql = '
			SELECT rolname, rolsuper, rolcreatedb, rolcreaterole, rolinherit,
				rolcanlogin, rolconnlimit, rolvaliduntil, rolconfig
			FROM pg_catalog.pg_roles';
        if ($rolename) {
            $sql .= " WHERE rolname!='{$rolename}'";
        }

        $sql .= ' ORDER BY rolname';

        return $this->selectSet($sql);
    }

    /**
     * Returns information about a single role
     *
     * @param $rolename The name of the role to retrieve
     * @return The role's data
     */
    public function getRole($rolename)
    {
        $this->clean($rolename);

        $sql = "
			SELECT rolname, rolsuper, rolcreatedb, rolcreaterole, rolinherit,
				rolcanlogin, rolconnlimit, rolvaliduntil, rolconfig
			FROM pg_catalog.pg_roles WHERE rolname='{$rolename}'";

        return $this->selectSet($sql);
    }

    /**
     * Returns all users in the database cluster
     *
     * @return All users
     */
    public function getUsers()
    {
        $sql = 'SELECT usename, usesuper, usecreatedb, valuntil AS useexpires, useconfig
			FROM pg_user
			ORDER BY usename';

        return $this->selectSet($sql);
    }

    /**
     * Returns information about a single user
     *
     * @param $username The username of the user to retrieve
     * @return The user's data
     */
    public function getUser($username)
    {
        $this->clean($username);

        $sql = "SELECT usename, usesuper, usecreatedb, valuntil AS useexpires, useconfig
			FROM pg_user
			WHERE usename='{$username}'";

        return $this->selectSet($sql);
    }

    /**
     * Creates a new role
     *
     * @param $rolename     The name of the role to create
     * @param $password     A password for the role
     * @param $superuser    Boolean whether or not the role is a superuser
     * @param $createdb     Boolean whether or not the role can create databases
     * @param $createrole   Boolean whether or not the role can create other roles
     * @param $inherits     Boolean whether or not the role inherits the privileges from parent roles
     * @param $login        Boolean whether or not the role will be allowed to login
     * @param $connlimit    Number of concurrent connections the role can make
     * @param $expiry       String Format 'YYYY-MM-DD HH:MM:SS'.  '' means never expire
     * @param $memberof     (array) Roles to which the new role will be immediately added as a new member
     * @param $members      (array) Roles which are automatically added as members of the new role
     * @param $adminmembers (array) Roles which are automatically added as admin members of the new role
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function createRole(
        $rolename,
        $password,
        $superuser,
        $createdb,
        $createrole,
        $inherits,
        $login,
        $connlimit,
        $expiry,
        $memberof,
        $members,
        $adminmembers
    ) {
        $enc = $this->_encryptPassword($rolename, $password);
        $this->fieldClean($rolename);
        $this->clean($enc);
        $this->clean($connlimit);
        $this->clean($expiry);
        $this->fieldArrayClean($memberof);
        $this->fieldArrayClean($members);
        $this->fieldArrayClean($adminmembers);

        $sql = "CREATE ROLE \"{$rolename}\"";
        if ($password != '') {
            $sql .= " WITH ENCRYPTED PASSWORD '{$enc}'";
        }

        $sql .= $superuser ? ' SUPERUSER' : ' NOSUPERUSER';
        $sql .= $createdb ? ' CREATEDB' : ' NOCREATEDB';
        $sql .= $createrole ? ' CREATEROLE' : ' NOCREATEROLE';
        $sql .= $inherits ? ' INHERIT' : ' NOINHERIT';
        $sql .= $login ? ' LOGIN' : ' NOLOGIN';
        if ($connlimit != '') {
            $sql .= " CONNECTION LIMIT {$connlimit}";
        } else {
            $sql .= ' CONNECTION LIMIT -1';
        }

        if ($expiry != '') {
            $sql .= " VALID UNTIL '{$expiry}'";
        } else {
            $sql .= " VALID UNTIL 'infinity'";
        }

        if (is_array($memberof) && sizeof($memberof) > 0) {
            $sql .= ' IN ROLE "' . join('", "', $memberof) . '"';
        }

        if (is_array($members) && sizeof($members) > 0) {
            $sql .= ' ROLE "' . join('", "', $members) . '"';
        }

        if (is_array($adminmembers) && sizeof($adminmembers) > 0) {
            $sql .= ' ADMIN "' . join('", "', $adminmembers) . '"';
        }

        return $this->execute($sql);
    }

    /**
     * Helper function that computes encypted PostgreSQL passwords
     *
     * @param $username The username
     * @param $password The password
     * @return string
     */
    public function _encryptPassword($username, $password)
    {
        return 'md5' . md5($password . $username);
    }

    /**
     * Adjusts a role's info and renames it
     *
     * @param $rolename        The name of the role to adjust
     * @param $password        A password for the role
     * @param $superuser       Boolean whether or not the role is a superuser
     * @param $createdb        Boolean whether or not the role can create databases
     * @param $createrole      Boolean whether or not the role can create other roles
     * @param $inherits        Boolean whether or not the role inherits the privileges from parent roles
     * @param $login           Boolean whether or not the role will be allowed to login
     * @param $connlimit       Number of concurrent connections the role can make
     * @param $expiry          string Format 'YYYY-MM-DD HH:MM:SS'.  '' means never expire
     * @param $memberof        (array) Roles to which the role will be immediately added as a new member
     * @param $members         (array) Roles which are automatically added as members of the role
     * @param $adminmembers    (array) Roles which are automatically added as admin members of the role
     * @param $memberofold     (array) Original roles whose the role belongs to
     * @param $membersold      (array) Original roles that are members of the role
     * @param $adminmembersold (array) Original roles that are admin members of the role
     * @param $newrolename     The new name of the role
     * @return bool|int 0 success
     */
    public function setRenameRole(
        $rolename,
        $password,
        $superuser,
        $createdb,
        $createrole,
        $inherits,
        $login,
        $connlimit,
        $expiry,
        $memberof,
        $members,
        $adminmembers,
        $memberofold,
        $membersold,
        $adminmembersold,
        $newrolename
    ) {

        $status = $this->beginTransaction();
        if ($status != 0) {
            return -1;
        }

        if ($rolename != $newrolename) {
            $status = $this->renameRole($rolename, $newrolename);
            if ($status != 0) {
                $this->rollbackTransaction();

                return -3;
            }
            $rolename = $newrolename;
        }

        $status =
        $this->setRole($rolename, $password, $superuser, $createdb, $createrole, $inherits, $login, $connlimit, $expiry, $memberof, $members,
            $adminmembers, $memberofold, $membersold, $adminmembersold);
        if ($status != 0) {
            $this->rollbackTransaction();

            return -2;
        }

        return $this->endTransaction();
    }

    /**
     * Renames a role
     *
     * @param $rolename    The name of the role to rename
     * @param $newrolename The new name of the role
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function renameRole($rolename, $newrolename)
    {
        $this->fieldClean($rolename);
        $this->fieldClean($newrolename);

        $sql = "ALTER ROLE \"{$rolename}\" RENAME TO \"{$newrolename}\"";

        return $this->execute($sql);
    }

    /**
     * Adjusts a role's info
     *
     * @param $rolename        The name of the role to adjust
     * @param $password        A password for the role
     * @param $superuser       Boolean whether or not the role is a superuser
     * @param $createdb        Boolean whether or not the role can create databases
     * @param $createrole      Boolean whether or not the role can create other roles
     * @param $inherits        Boolean whether or not the role inherits the privileges from parent roles
     * @param $login           Boolean whether or not the role will be allowed to login
     * @param $connlimit       Number of concurrent connections the role can make
     * @param $expiry          string Format 'YYYY-MM-DD HH:MM:SS'.  '' means never expire
     * @param $memberof        (array) Roles to which the role will be immediately added as a new member
     * @param $members         (array) Roles which are automatically added as members of the role
     * @param $adminmembers    (array) Roles which are automatically added as admin members of the role
     * @param $memberofold     (array) Original roles whose the role belongs to
     * @param $membersold      (array) Original roles that are members of the role
     * @param $adminmembersold (array) Original roles that are admin members of the role
     * @return int|\PHPPgAdmin\Database\A 0 success
     */
    public function setRole(
        $rolename,
        $password,
        $superuser,
        $createdb,
        $createrole,
        $inherits,
        $login,
        $connlimit,
        $expiry,
        $memberof,
        $members,
        $adminmembers,
        $memberofold,
        $membersold,
        $adminmembersold
    ) {
        $enc = $this->_encryptPassword($rolename, $password);
        $this->fieldClean($rolename);
        $this->clean($enc);
        $this->clean($connlimit);
        $this->clean($expiry);
        $this->fieldArrayClean($memberof);
        $this->fieldArrayClean($members);
        $this->fieldArrayClean($adminmembers);

        $sql = "ALTER ROLE \"{$rolename}\"";
        if ($password != '') {
            $sql .= " WITH ENCRYPTED PASSWORD '{$enc}'";
        }

        $sql .= $superuser ? ' SUPERUSER' : ' NOSUPERUSER';
        $sql .= $createdb ? ' CREATEDB' : ' NOCREATEDB';
        $sql .= $createrole ? ' CREATEROLE' : ' NOCREATEROLE';
        $sql .= $inherits ? ' INHERIT' : ' NOINHERIT';
        $sql .= $login ? ' LOGIN' : ' NOLOGIN';
        if ($connlimit != '') {
            $sql .= " CONNECTION LIMIT {$connlimit}";
        } else {
            $sql .= ' CONNECTION LIMIT -1';
        }

        if ($expiry != '') {
            $sql .= " VALID UNTIL '{$expiry}'";
        } else {
            $sql .= " VALID UNTIL 'infinity'";
        }

        $status = $this->execute($sql);

        if ($status != 0) {
            return -1;
        }

        //memberof
        $old = explode(',', $memberofold);
        foreach ($memberof as $m) {
            if (!in_array($m, $old)) {
                $status = $this->grantRole($m, $rolename);
                if ($status != 0) {
                    return -1;
                }
            }
        }
        if ($memberofold) {
            foreach ($old as $o) {
                if (!in_array($o, $memberof)) {
                    $status = $this->revokeRole($o, $rolename, 0, 'CASCADE');
                    if ($status != 0) {
                        return -1;
                    }
                }
            }
        }

        //members
        $old = explode(',', $membersold);
        foreach ($members as $m) {
            if (!in_array($m, $old)) {
                $status = $this->grantRole($rolename, $m);
                if ($status != 0) {
                    return -1;
                }
            }
        }
        if ($membersold) {
            foreach ($old as $o) {
                if (!in_array($o, $members)) {
                    $status = $this->revokeRole($rolename, $o, 0, 'CASCADE');
                    if ($status != 0) {
                        return -1;
                    }
                }
            }
        }

        //adminmembers
        $old = explode(',', $adminmembersold);
        foreach ($adminmembers as $m) {
            if (!in_array($m, $old)) {
                $status = $this->grantRole($rolename, $m, 1);
                if ($status != 0) {
                    return -1;
                }
            }
        }
        if ($adminmembersold) {
            foreach ($old as $o) {
                if (!in_array($o, $adminmembers)) {
                    $status = $this->revokeRole($rolename, $o, 1, 'CASCADE');
                    if ($status != 0) {
                        return -1;
                    }
                }
            }
        }

        return $status;
    }

    /**
     * Grants membership in a role
     *
     * @param     $role     The name of the target role
     * @param     $rolename The name of the role that will belong to the target role
     * @param int $admin    (optional) Flag to grant the admin option
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function grantRole($role, $rolename, $admin = 0)
    {
        $this->fieldClean($role);
        $this->fieldClean($rolename);

        $sql = "GRANT \"{$role}\" TO \"{$rolename}\"";
        if ($admin == 1) {
            $sql .= ' WITH ADMIN OPTION';
        }

        return $this->execute($sql);
    }

    /**
     * Revokes membership in a role
     *
     * @param        $role     The name of the target role
     * @param        $rolename The name of the role that will not belong to the target role
     * @param int    $admin    (optional) Flag to revoke only the admin option
     * @param string $type     (optional) Type of revoke: RESTRICT | CASCADE
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function revokeRole($role, $rolename, $admin = 0, $type = 'RESTRICT')
    {
        $this->fieldClean($role);
        $this->fieldClean($rolename);

        $sql = 'REVOKE ';
        if ($admin == 1) {
            $sql .= 'ADMIN OPTION FOR ';
        }

        $sql .= "\"{$role}\" FROM \"{$rolename}\" {$type}";

        return $this->execute($sql);
    }

    /**
     * Removes a role
     *
     * @param $rolename The name of the role to drop
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function dropRole($rolename)
    {
        $this->fieldClean($rolename);

        $sql = "DROP ROLE \"{$rolename}\"";

        return $this->execute($sql);
    }

    /**
     * Creates a new user
     *
     * @param $username   The username of the user to create
     * @param $password   A password for the user
     * @param $createdb   boolean Whether or not the user can create databases
     * @param $createuser boolean Whether or not the user can create other users
     * @param $expiry     string Format 'YYYY-MM-DD HH:MM:SS'.  '' means never expire
     * @param $groups
     * @return \PHPPgAdmin\Database\A 0 success
     * @internal param $group (array) The groups to create the user in
     */
    public function createUser($username, $password, $createdb, $createuser, $expiry, $groups)
    {
        $enc = $this->_encryptPassword($username, $password);
        $this->fieldClean($username);
        $this->clean($enc);
        $this->clean($expiry);
        $this->fieldArrayClean($groups);

        $sql = "CREATE USER \"{$username}\"";
        if ($password != '') {
            $sql .= " WITH ENCRYPTED PASSWORD '{$enc}'";
        }

        $sql .= $createdb ? ' CREATEDB' : ' NOCREATEDB';
        $sql .= $createuser ? ' CREATEUSER' : ' NOCREATEUSER';
        if (is_array($groups) && sizeof($groups) > 0) {
            $sql .= ' IN GROUP "' . join('", "', $groups) . '"';
        }

        if ($expiry != '') {
            $sql .= " VALID UNTIL '{$expiry}'";
        } else {
            $sql .= " VALID UNTIL 'infinity'";
        }

        return $this->execute($sql);
    }

    /**
     * Adjusts a user's info and renames the user
     *
     * @param $username   The username of the user to modify
     * @param $password   A new password for the user
     * @param $createdb   boolean Whether or not the user can create databases
     * @param $createuser boolean Whether or not the user can create other users
     * @param $expiry     string Format 'YYYY-MM-DD HH:MM:SS'.  '' means never expire.
     * @param $newname    The new name of the user
     * @return bool|int 0 success
     */
    public function setRenameUser($username, $password, $createdb, $createuser, $expiry, $newname)
    {
        $status = $this->beginTransaction();
        if ($status != 0) {
            return -1;
        }

        if ($username != $newname) {
            $status = $this->renameUser($username, $newname);
            if ($status != 0) {
                $this->rollbackTransaction();

                return -3;
            }
            $username = $newname;
        }

        $status = $this->setUser($username, $password, $createdb, $createuser, $expiry);
        if ($status != 0) {
            $this->rollbackTransaction();

            return -2;
        }

        return $this->endTransaction();
    }

    /**
     * Renames a user
     *
     * @param $username The username of the user to rename
     * @param $newname  The new name of the user
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function renameUser($username, $newname)
    {
        $this->fieldClean($username);
        $this->fieldClean($newname);

        $sql = "ALTER USER \"{$username}\" RENAME TO \"{$newname}\"";

        return $this->execute($sql);
    }

    // Tablespace functions

    /**
     * Adjusts a user's info
     *
     * @param $username   The username of the user to modify
     * @param $password   A new password for the user
     * @param $createdb   boolean Whether or not the user can create databases
     * @param $createuser boolean Whether or not the user can create other users
     * @param $expiry     string Format 'YYYY-MM-DD HH:MM:SS'.  '' means never expire.
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function setUser($username, $password, $createdb, $createuser, $expiry)
    {
        $enc = $this->_encryptPassword($username, $password);
        $this->fieldClean($username);
        $this->clean($enc);
        $this->clean($expiry);

        $sql = "ALTER USER \"{$username}\"";
        if ($password != '') {
            $sql .= " WITH ENCRYPTED PASSWORD '{$enc}'";
        }

        $sql .= $createdb ? ' CREATEDB' : ' NOCREATEDB';
        $sql .= $createuser ? ' CREATEUSER' : ' NOCREATEUSER';
        if ($expiry != '') {
            $sql .= " VALID UNTIL '{$expiry}'";
        } else {
            $sql .= " VALID UNTIL 'infinity'";
        }

        return $this->execute($sql);
    }

    /**
     * Removes a user
     *
     * @param $username The username of the user to drop
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function dropUser($username)
    {
        $this->fieldClean($username);

        $sql = "DROP USER \"{$username}\"";

        return $this->execute($sql);
    }

    /**
     * Changes a role's password
     *
     * @param $rolename The role name
     * @param $password The new password
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function changePassword($rolename, $password)
    {
        $enc = $this->_encryptPassword($rolename, $password);
        $this->fieldClean($rolename);
        $this->clean($enc);

        $sql = "ALTER ROLE \"{$rolename}\" WITH ENCRYPTED PASSWORD '{$enc}'";

        return $this->execute($sql);
    }

    /**
     * Adds a group member
     *
     * @param $groname The name of the group
     * @param $user    The name of the user to add to the group
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function addGroupMember($groname, $user)
    {
        $this->fieldClean($groname);
        $this->fieldClean($user);

        $sql = "ALTER GROUP \"{$groname}\" ADD USER \"{$user}\"";

        return $this->execute($sql);
    }

    /**
     * Returns all role names which the role belongs to
     *
     * @param $rolename The role name
     * @return All role names
     */
    public function getMemberOf($rolename)
    {
        $this->clean($rolename);

        $sql = "
			SELECT rolname FROM pg_catalog.pg_roles R, pg_auth_members M
			WHERE R.oid=M.roleid
				AND member IN (
					SELECT oid FROM pg_catalog.pg_roles
					WHERE rolname='{$rolename}')
			ORDER BY rolname";

        return $this->selectSet($sql);
    }

    // Administration functions

    /**
     * Returns all role names that are members of a role
     *
     * @param $rolename The role name
     * @param $admin    (optional) Find only admin members
     * @return All role names
     */
    public function getMembers($rolename, $admin = 'f')
    {
        $this->clean($rolename);

        $sql = "
			SELECT rolname FROM pg_catalog.pg_roles R, pg_auth_members M
			WHERE R.oid=M.member AND admin_option='{$admin}'
				AND roleid IN (SELECT oid FROM pg_catalog.pg_roles
					WHERE rolname='{$rolename}')
			ORDER BY rolname";

        return $this->selectSet($sql);
    }

    /**
     * Removes a group member
     *
     * @param $groname The name of the group
     * @param $user    The name of the user to remove from the group
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function dropGroupMember($groname, $user)
    {
        $this->fieldClean($groname);
        $this->fieldClean($user);

        $sql = "ALTER GROUP \"{$groname}\" DROP USER \"{$user}\"";

        return $this->execute($sql);
    }

    /**
     * Return users in a specific group
     *
     * @param $groname The name of the group
     * @return All users in the group
     */
    public function getGroup($groname)
    {
        $this->clean($groname);

        $sql = "
			SELECT s.usename FROM pg_catalog.pg_user s, pg_catalog.pg_group g
			WHERE g.groname='{$groname}' AND s.usesysid = ANY (g.grolist)
			ORDER BY s.usename";

        return $this->selectSet($sql);
    }

    /**
     * Returns all groups in the database cluser
     *
     * @return All groups
     */
    public function getGroups()
    {
        $sql = 'SELECT groname FROM pg_group ORDER BY groname';

        return $this->selectSet($sql);
    }

    /**
     * Creates a new group
     *
     * @param $groname The name of the group
     * @param $users   An array of users to add to the group
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function createGroup($groname, $users)
    {
        $this->fieldClean($groname);

        $sql = "CREATE GROUP \"{$groname}\"";

        if (is_array($users) && sizeof($users) > 0) {
            $this->fieldArrayClean($users);
            $sql .= ' WITH USER "' . join('", "', $users) . '"';
        }

        return $this->execute($sql);
    }

    /**
     * Removes a group
     *
     * @param $groname The name of the group to drop
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function dropGroup($groname)
    {
        $this->fieldClean($groname);

        $sql = "DROP GROUP \"{$groname}\"";

        return $this->execute($sql);
    }

    /**
     * Grants a privilege to a user, group or public
     *
     * @param $mode        'GRANT' or 'REVOKE';
     * @param $type        The type of object
     * @param $object      The name of the object
     * @param $public      True to grant to public, false otherwise
     * @param $usernames   The array of usernames to grant privs to.
     * @param $groupnames  The array of group names to grant privs to.
     * @param $privileges  The array of privileges to grant (eg. ('SELECT', 'ALL PRIVILEGES', etc.) )
     * @param $grantoption True if has grant option, false otherwise
     * @param $cascade     True for cascade revoke, false otherwise
     * @param $table       the column's table if type=column
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function setPrivileges(
        $mode,
        $type,
        $object,
        $public,
        $usernames,
        $groupnames,
        $privileges,
        $grantoption,
        $cascade,
        $table
    ) {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldArrayClean($usernames);
        $this->fieldArrayClean($groupnames);

        // Input checking
        if (!is_array($privileges) || sizeof($privileges) == 0) {
            return -3;
        }

        if (!is_array($usernames) || !is_array($groupnames) ||
            (!$public && sizeof($usernames) == 0 && sizeof($groupnames) == 0)) {
            return -4;
        }

        if ($mode != 'GRANT' && $mode != 'REVOKE') {
            return -5;
        }

        $sql = $mode;

        // Grant option
        if ($this->hasGrantOption() && $mode == 'REVOKE' && $grantoption) {
            $sql .= ' GRANT OPTION FOR';
        }

        if (in_array('ALL PRIVILEGES', $privileges)) {
            $sql .= ' ALL PRIVILEGES';
        } else {
            if ($type == 'column') {
                $this->fieldClean($object);
                $sql .= ' ' . join(" (\"{$object}\"), ", $privileges);
            } else {
                $sql .= ' ' . join(', ', $privileges);
            }
        }

        switch ($type) {
            case 'column':
                $sql .= " (\"{$object}\")";
                $object = $table;
            case 'table':
            case 'view':
            case 'sequence':
                $this->fieldClean($object);
                $sql .= " ON \"{$f_schema}\".\"{$object}\"";
                break;
            case 'database':
                $this->fieldClean($object);
                $sql .= " ON DATABASE \"{$object}\"";
                break;
            case 'function':
                // Function comes in with $object as function OID
                $fn = $this->getFunction($object);
                $this->fieldClean($fn->fields['proname']);
                $sql .= " ON FUNCTION \"{$f_schema}\".\"{$fn->fields['proname']}\"({$fn->fields['proarguments']})";
                break;
            case 'language':
                $this->fieldClean($object);
                $sql .= " ON LANGUAGE \"{$object}\"";
                break;
            case 'schema':
                $this->fieldClean($object);
                $sql .= " ON SCHEMA \"{$object}\"";
                break;
            case 'tablespace':
                $this->fieldClean($object);
                $sql .= " ON TABLESPACE \"{$object}\"";
                break;
            default:
                return -1;
        }

        // Dump PUBLIC
        $first = true;
        $sql .= ($mode == 'GRANT') ? ' TO ' : ' FROM ';
        if ($public) {
            $sql .= 'PUBLIC';
            $first = false;
        }
        // Dump users
        foreach ($usernames as $v) {
            if ($first) {
                $sql .= "\"{$v}\"";
                $first = false;
            } else {
                $sql .= ", \"{$v}\"";
            }
        }
        // Dump groups
        foreach ($groupnames as $v) {
            if ($first) {
                $sql .= "GROUP \"{$v}\"";
                $first = false;
            } else {
                $sql .= ", GROUP \"{$v}\"";
            }
        }

        // Grant option
        if ($this->hasGrantOption() && $mode == 'GRANT' && $grantoption) {
            $sql .= ' WITH GRANT OPTION';
        }

        // Cascade revoke
        if ($this->hasGrantOption() && $mode == 'REVOKE' && $cascade) {
            $sql .= ' CASCADE';
        }

        return $this->execute($sql);
    }

    /**
     * Retrieves information for all tablespaces
     *
     * @param bool|\PHPPgAdmin\Database\Include $all Include all tablespaces (necessary when moving objects back to the default space)
     * @return \PHPPgAdmin\Database\A recordset
     */
    public function getTablespaces($all = false)
    {
        $conf = $this->conf;

        $sql = "SELECT spcname, pg_catalog.pg_get_userbyid(spcowner) AS spcowner, pg_catalog.pg_tablespace_location(oid) as spclocation,
                    (SELECT description FROM pg_catalog.pg_shdescription pd WHERE pg_tablespace.oid=pd.objoid AND pd.classoid='pg_tablespace'::regclass) AS spccomment
					FROM pg_catalog.pg_tablespace";

        if (!$conf['show_system'] && !$all) {
            $sql .= ' WHERE spcname NOT LIKE $$pg\_%$$';
        }

        $sql .= ' ORDER BY spcname';

        return $this->selectSet($sql);
    }

    // Misc functions

    /**
     * Retrieves a tablespace's information
     *
     * @param $spcname
     * @return \PHPPgAdmin\Database\A recordset
     */
    public function getTablespace($spcname)
    {
        $this->clean($spcname);

        $sql = "SELECT spcname, pg_catalog.pg_get_userbyid(spcowner) AS spcowner, pg_catalog.pg_tablespace_location(oid) as spclocation,
                    (SELECT description FROM pg_catalog.pg_shdescription pd WHERE pg_tablespace.oid=pd.objoid AND pd.classoid='pg_tablespace'::regclass) AS spccomment
					FROM pg_catalog.pg_tablespace WHERE spcname='{$spcname}'";

        return $this->selectSet($sql);
    }

    /**
     * Creates a tablespace
     *
     * @param        $spcname  The name of the tablespace to create
     * @param        $spcowner The owner of the tablespace. '' for current
     * @param        $spcloc   The directory in which to create the tablespace
     * @param string $comment
     * @return int 0 success
     */
    public function createTablespace($spcname, $spcowner, $spcloc, $comment = '')
    {
        $this->fieldClean($spcname);
        $this->clean($spcloc);

        $sql = "CREATE TABLESPACE \"{$spcname}\"";

        if ($spcowner != '') {
            $this->fieldClean($spcowner);
            $sql .= " OWNER \"{$spcowner}\"";
        }

        $sql .= " LOCATION '{$spcloc}'";

        $status = $this->execute($sql);
        if ($status != 0) {
            return -1;
        }

        if ($comment != '' && $this->hasSharedComments()) {
            $status = $this->setComment('TABLESPACE', $spcname, '', $comment);
            if ($status != 0) {
                return -2;
            }
        }

        return 0;
    }

    /**
     * Alters a tablespace
     *
     * @param        $spcname The name of the tablespace
     * @param        $name    The new name for the tablespace
     * @param        $owner   The new owner for the tablespace
     * @param string $comment
     * @return bool|int 0 success
     */
    public function alterTablespace($spcname, $name, $owner, $comment = '')
    {
        $this->fieldClean($spcname);
        $this->fieldClean($name);
        $this->fieldClean($owner);

        // Begin transaction
        $status = $this->beginTransaction();
        if ($status != 0) {
            return -1;
        }

        // Owner
        $sql    = "ALTER TABLESPACE \"{$spcname}\" OWNER TO \"{$owner}\"";
        $status = $this->execute($sql);
        if ($status != 0) {
            $this->rollbackTransaction();

            return -2;
        }

        // Rename (only if name has changed)
        if ($name != $spcname) {
            $sql    = "ALTER TABLESPACE \"{$spcname}\" RENAME TO \"{$name}\"";
            $status = $this->execute($sql);
            if ($status != 0) {
                $this->rollbackTransaction();

                return -3;
            }

            $spcname = $name;
        }

        // Set comment if it has changed
        if (trim($comment) != '' && $this->hasSharedComments()) {
            $status = $this->setComment('TABLESPACE', $spcname, '', $comment);
            if ($status != 0) {
                return -4;
            }
        }

        return $this->endTransaction();
    }

    /**
     * Drops a tablespace
     *
     * @param $spcname The name of the domain to drop
     * @return \PHPPgAdmin\Database\A 0 success
     */
    public function dropTablespace($spcname)
    {
        $this->fieldClean($spcname);

        $sql = "DROP TABLESPACE \"{$spcname}\"";

        return $this->execute($sql);
    }

    /**
     * Analyze a database
     *
     * @param $table (optional) The table to analyze
     * @return \PHPPgAdmin\Database\A
     */
    public function analyzeDB($table = '')
    {
        if ($table != '') {
            $f_schema = $this->_schema;
            $this->fieldClean($f_schema);
            $this->fieldClean($table);

            $sql = "ANALYZE \"{$f_schema}\".\"{$table}\"";
        } else {
            $sql = 'ANALYZE';
        }

        return $this->execute($sql);
    }

    /**
     * Vacuums a database
     *
     * @param \PHPPgAdmin\Database\The|string $table   The table to vacuum
     * @param bool|\PHPPgAdmin\Database\If    $analyze If true, also does analyze
     * @param bool|\PHPPgAdmin\Database\If    $full    If true, selects "full" vacuum
     * @param bool|\PHPPgAdmin\Database\If    $freeze  If true, selects aggressive "freezing" of tuples
     * @return \PHPPgAdmin\Database\A
     */
    public function vacuumDB($table = '', $analyze = false, $full = false, $freeze = false)
    {

        $sql = 'VACUUM';
        if ($full) {
            $sql .= ' FULL';
        }

        if ($freeze) {
            $sql .= ' FREEZE';
        }

        if ($analyze) {
            $sql .= ' ANALYZE';
        }

        if ($table != '') {
            $f_schema = $this->_schema;
            $this->fieldClean($f_schema);
            $this->fieldClean($table);
            $sql .= " \"{$f_schema}\".\"{$table}\"";
        }

        return $this->execute($sql);
    }

    /**
     * Returns all autovacuum global configuration
     *
     * @return associative array array( param => value, ...)
     */
    public function getAutovacuum()
    {

        $_defaults = $this->selectSet("SELECT name, setting
			FROM pg_catalog.pg_settings
			WHERE
				name = 'autovacuum'
				OR name = 'autovacuum_vacuum_threshold'
				OR name = 'autovacuum_vacuum_scale_factor'
				OR name = 'autovacuum_analyze_threshold'
				OR name = 'autovacuum_analyze_scale_factor'
				OR name = 'autovacuum_vacuum_cost_delay'
				OR name = 'autovacuum_vacuum_cost_limit'
				OR name = 'vacuum_freeze_min_age'
				OR name = 'autovacuum_freeze_max_age'
			"
        );

        $ret = [];
        while (!$_defaults->EOF) {
            $ret[$_defaults->fields['name']] = $_defaults->fields['setting'];
            $_defaults->moveNext();
        }

        return $ret;
    }

    /**
     * Returns all available autovacuum per table information.
     *
     * @param $table
     * @param $vacenabled
     * @param $vacthreshold
     * @param $vacscalefactor
     * @param $anathresold
     * @param $anascalefactor
     * @param $vaccostdelay
     * @param $vaccostlimit
     * @return \PHPPgAdmin\Database\A recordset
     */
    public function saveAutovacuum(
        $table,
        $vacenabled,
        $vacthreshold,
        $vacscalefactor,
        $anathresold,
        $anascalefactor,
        $vaccostdelay,
        $vaccostlimit
    ) {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($table);

        $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" SET (";

        if (!empty($vacenabled)) {
            $this->clean($vacenabled);
            $params[] = "autovacuum_enabled='{$vacenabled}'";
        }
        if (!empty($vacthreshold)) {
            $this->clean($vacthreshold);
            $params[] = "autovacuum_vacuum_threshold='{$vacthreshold}'";
        }
        if (!empty($vacscalefactor)) {
            $this->clean($vacscalefactor);
            $params[] = "autovacuum_vacuum_scale_factor='{$vacscalefactor}'";
        }
        if (!empty($anathresold)) {
            $this->clean($anathresold);
            $params[] = "autovacuum_analyze_threshold='{$anathresold}'";
        }
        if (!empty($anascalefactor)) {
            $this->clean($anascalefactor);
            $params[] = "autovacuum_analyze_scale_factor='{$anascalefactor}'";
        }
        if (!empty($vaccostdelay)) {
            $this->clean($vaccostdelay);
            $params[] = "autovacuum_vacuum_cost_delay='{$vaccostdelay}'";
        }
        if (!empty($vaccostlimit)) {
            $this->clean($vaccostlimit);
            $params[] = "autovacuum_vacuum_cost_limit='{$vaccostlimit}'";
        }

        $sql = $sql . implode(',', $params) . ');';

        return $this->execute($sql);
    }

    // Type conversion routines

    public function dropAutovacuum($table)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($table);

        return $this->execute("
			ALTER TABLE \"{$f_schema}\".\"{$table}\" RESET (autovacuum_enabled, autovacuum_vacuum_threshold,
				autovacuum_vacuum_scale_factor, autovacuum_analyze_threshold, autovacuum_analyze_scale_factor,
				autovacuum_vacuum_cost_delay, autovacuum_vacuum_cost_limit
			);"
        );
    }

    /**
     * Returns all available process information.
     *
     * @param $database (optional) Find only connections to specified database
     * @return A recordset
     */
    public function getProcesses($database = null)
    {
        if ($database === null) {
            $sql = "SELECT datname, usename, pid, waiting, state_change as query_start,
                  case when state='idle in transaction' then '<IDLE> in transaction' when state = 'idle' then '<IDLE>' else query end as query
				FROM pg_catalog.pg_stat_activity
				ORDER BY datname, usename, pid";
        } else {
            $this->clean($database);
            $sql = "SELECT datname, usename, pid, waiting, state_change as query_start,
                  case when state='idle in transaction' then '<IDLE> in transaction' when state = 'idle' then '<IDLE>' else query end as query
				FROM pg_catalog.pg_stat_activity
				WHERE datname='{$database}'
				ORDER BY usename, pid";
        }

        return $this->selectSet($sql);
    }

    // interfaces Statistics collector functions

    /**
     * Returns table locks information in the current database
     *
     * @return A recordset
     */

    public function getLocks()
    {
        $conf = $this->conf;

        if (!$conf['show_system']) {
            $where = 'AND pn.nspname NOT LIKE $$pg\_%$$';
        } else {
            $where = "AND nspname !~ '^pg_t(emp_[0-9]+|oast)$'";
        }

        $sql = "
			SELECT
				pn.nspname, pc.relname AS tablename, pl.pid, pl.mode, pl.granted, pl.virtualtransaction,
				(select transactionid from pg_catalog.pg_locks l2 where l2.locktype='transactionid'
					and l2.mode='ExclusiveLock' and l2.virtualtransaction=pl.virtualtransaction) as transaction
			FROM
				pg_catalog.pg_locks pl,
				pg_catalog.pg_class pc,
				pg_catalog.pg_namespace pn
			WHERE
				pl.relation = pc.oid AND pc.relnamespace=pn.oid
			{$where}
			ORDER BY pid,nspname,tablename";

        return $this->selectSet($sql);
    }

    /**
     * Sends a cancel or kill command to a process
     *
     * @param $pid    The ID of the backend process
     * @param $signal 'CANCEL'
     * @return int 0 success
     */
    public function sendSignal($pid, $signal)
    {
        // Clean
        $pid = (int) $pid;

        if ($signal == 'CANCEL') {
            $sql = "SELECT pg_catalog.pg_cancel_backend({$pid}) AS val";
        } elseif ($signal == 'KILL') {
            $sql = "SELECT pg_catalog.pg_terminate_backend({$pid}) AS val";
        } else {
            return -1;
        }

        // Execute the query
        $val = $this->selectField($sql, 'val');

        if ($val === 'f') {
            return -1;
        }

        if ($val === 't') {
            return 0;
        } else {
            return -1;
        }
    }

    /**
     * Executes an SQL script as a series of SQL statements.  Returns
     * the result of the final step.  This is a very complicated lexer
     * based on the REL7_4_STABLE src/bin/psql/mainloop.c lexer in
     * the PostgreSQL source code.
     * XXX: It does not handle multibyte languages properly.
     *
     * @param $name     Entry in $_FILES to use
     * @param $callback (optional) Callback function to call with each query,
     *                  its result and line number.
     * @return True for general success, false on any failure.
     */
    public function executeScript($name, $callback = null)
    {

        // This whole function isn't very encapsulated, but hey...
        $conn = $this->conn->_connectionID;
        if (!is_uploaded_file($_FILES[$name]['tmp_name'])) {
            return false;
        }

        $fd = fopen($_FILES[$name]['tmp_name'], 'rb');
        if (!$fd) {
            return false;
        }

        // Build up each SQL statement, they can be multiline
        $query_buf    = null;
        $query_start  = 0;
        $in_quote     = 0;
        $in_xcomment  = 0;
        $bslash_count = 0;
        $dol_quote    = null;
        $paren_level  = 0;
        $len          = 0;
        $i            = 0;
        $prevlen      = 0;
        $thislen      = 0;
        $lineno       = 0;

        // Loop over each line in the file
        while (!feof($fd)) {
            $line = fgets($fd);
            $lineno++;

            // Nothing left on line? Then ignore...
            if (trim($line) == '') {
                continue;
            }

            $len         = strlen($line);
            $query_start = 0;

            /*
             * Parse line, looking for command separators.
             *
             * The current character is at line[i], the prior character at line[i
             * - prevlen], the next character at line[i + thislen].
             */
            $prevlen = 0;
            $thislen = ($len > 0) ? 1 : 0;

            for ($i = 0; $i < $len; $this->advance_1($i, $prevlen, $thislen)) {

                /* was the previous character a backslash? */
                if ($i > 0 && substr($line, $i - $prevlen, 1) == '\\') {
                    $bslash_count++;
                } else {
                    $bslash_count = 0;
                }

                /*
                 * It is important to place the in_* test routines before the
                 * in_* detection routines. i.e. we have to test if we are in
                 * a quote before testing for comments.
                 */

                /* in quote? */
                if ($in_quote !== 0) {
                    /*
                     * end of quote if matching non-backslashed character.
                     * backslashes don't count for double quotes, though.
                     */
                    if (substr($line, $i, 1) == $in_quote &&
                        ($bslash_count % 2 == 0 || $in_quote == '"')) {
                        $in_quote = 0;
                    }
                } /* in or end of $foo$ type quote? */
                else {
                    if ($dol_quote) {
                        if (strncmp(substr($line, $i), $dol_quote, strlen($dol_quote)) == 0) {
                            $this->advance_1($i, $prevlen, $thislen);
                            while (substr($line, $i, 1) != '$') {
                                $this->advance_1($i, $prevlen, $thislen);
                            }

                            $dol_quote = null;
                        }
                    } /* start of extended comment? */
                    else {
                        if (substr($line, $i, 2) == '/*') {
                            $in_xcomment++;
                            if ($in_xcomment == 1) {
                                $this->advance_1($i, $prevlen, $thislen);
                            }
                        } /* in or end of extended comment? */
                        else {
                            if ($in_xcomment) {
                                if (substr($line, $i, 2) == '*/' && !--$in_xcomment) {
                                    $this->advance_1($i, $prevlen, $thislen);
                                }
                            } /* start of quote? */
                            else {
                                if (substr($line, $i, 1) == '\'' || substr($line, $i, 1) == '"') {
                                    $in_quote = substr($line, $i, 1);
                                } /*
                                 * start of $foo$ type quote?
                                 */
                                else {
                                    if (!$dol_quote && $this->valid_dolquote(substr($line, $i))) {
                                        $dol_end   = strpos(substr($line, $i + 1), '$');
                                        $dol_quote = substr($line, $i, $dol_end + 1);
                                        $this->advance_1($i, $prevlen, $thislen);
                                        while (substr($line, $i, 1) != '$') {
                                            $this->advance_1($i, $prevlen, $thislen);
                                        }
                                    } /* single-line comment? truncate line */
                                    else {
                                        if (substr($line, $i, 2) == '--') {
                                            $line = substr($line, 0, $i); /* remove comment */
                                            break;
                                        } /* count nested parentheses */

                                        if (substr($line, $i, 1) == '(') {
                                            $paren_level++;
                                        } else {
                                            if (substr($line, $i, 1) == ')' && $paren_level > 0) {
                                                $paren_level--;
                                            } /* semicolon? then send query */
                                            else {
                                                if (substr($line, $i, 1) == ';' && !$bslash_count && !$paren_level) {
                                                    $subline = substr(substr($line, 0, $i), $query_start);
                                                    /*
                                                     * insert a cosmetic newline, if this is not the first
                                                     * line in the buffer
                                                     */
                                                    if (strlen($query_buf) > 0) {
                                                        $query_buf .= "\n";
                                                    }

                                                    /* append the line to the query buffer */
                                                    $query_buf .= $subline;
                                                    /* is there anything in the query_buf? */
                                                    if (trim($query_buf)) {
                                                        $query_buf .= ';';

                                                        // Execute the query. PHP cannot execute
                                                        // empty queries, unlike libpq
                                                        $res = @pg_query($conn, $query_buf);

                                                        // Call the callback function for display
                                                        if ($callback !== null) {
                                                            $callback($query_buf, $res, $lineno);
                                                        }

                                                        // Check for COPY request
                                                        if (pg_result_status($res) == 4) {
                                                            // 4 == PGSQL_COPY_FROM
                                                            while (!feof($fd)) {
                                                                $copy = fgets($fd, 32768);
                                                                $lineno++;
                                                                pg_put_line($conn, $copy);
                                                                if ($copy == "\\.\n" || $copy == "\\.\r\n") {
                                                                    pg_end_copy($conn);
                                                                    break;
                                                                }
                                                            }
                                                        }
                                                    }
                                                    $query_buf   = null;
                                                    $query_start = $i + $thislen;
                                                }

                                                /*
                                                 * keyword or identifier?
                                                 * We grab the whole string so that we don't
                                                 * mistakenly see $foo$ inside an identifier as the start
                                                 * of a dollar quote.
                                                 */
                                                // XXX: multibyte here
                                                else {
                                                    if (preg_match('/^[_[:alpha:]]$/', substr($line, $i, 1))) {
                                                        $sub = substr($line, $i, $thislen);
                                                        while (preg_match('/^[\$_A-Za-z0-9]$/', $sub)) {
                                                            /* keep going while we still have identifier chars */
                                                            $this->advance_1($i, $prevlen, $thislen);
                                                            $sub = substr($line, $i, $thislen);
                                                        }
                                                        // Since we're now over the next character to be examined, it is necessary
                                                        // to move back one space.
                                                        $i -= $prevlen;
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            } // end for

            /* Put the rest of the line in the query buffer. */
            $subline = substr($line, $query_start);
            if ($in_quote || $dol_quote || strspn($subline, " \t\n\r") != strlen($subline)) {
                if (strlen($query_buf) > 0) {
                    $query_buf .= "\n";
                }

                $query_buf .= $subline;
            }

            $line = null;
        } // end while

        /*
         * Process query at the end of file without a semicolon, so long as
         * it's non-empty.
         */
        if (strlen($query_buf) > 0 && strspn($query_buf, " \t\n\r") != strlen($query_buf)) {
            // Execute the query
            $res = @pg_query($conn, $query_buf);

            // Call the callback function for display
            if ($callback !== null) {
                $callback($query_buf, $res, $lineno);
            }

            // Check for COPY request
            if (pg_result_status($res) == 4) {
                // 4 == PGSQL_COPY_FROM
                while (!feof($fd)) {
                    $copy = fgets($fd, 32768);
                    $lineno++;
                    pg_put_line($conn, $copy);
                    if ($copy == "\\.\n" || $copy == "\\.\r\n") {
                        pg_end_copy($conn);
                        break;
                    }
                }
            }
        }

        fclose($fd);

        return true;
    }

    /**
     * A private helper method for executeScript that advances the
     * character by 1.  In psql this is careful to take into account
     * multibyte languages, but we don't at the moment, so this function
     * is someone redundant, since it will always advance by 1
     *
     * @param &$i       The current character position in the line
     * @param &$prevlen Length of previous character (ie. 1)
     * @param &$thislen Length of current character (ie. 1)
     */
    private function advance_1(&$i, &$prevlen, &$thislen)
    {
        $prevlen = $thislen;
        $i += $thislen;
        $thislen = 1;
    }

    /**
     * Private helper method to detect a valid $foo$ quote delimiter at
     * the start of the parameter dquote
     *
     * @param $dquote
     * @return True if valid, false otherwise
     */
    private function valid_dolquote($dquote)
    {
        // XXX: support multibyte
        return (preg_match('/^[$][$]/', $dquote) || preg_match('/^[$][_[:alpha:]][_[:alnum:]]*[$]/', $dquote));
    }

    // Capabilities

    /**
     * Returns a recordset of all columns in a query.  Supports paging.
     *
     * @param $type       Either 'QUERY' if it is an SQL query, or 'TABLE' if it is a table identifier,
     *                    or 'SELECT" if it's a select query
     * @param $table      The base table of the query.  NULL for no table.
     * @param $query      The query that is being executed.  NULL for no query.
     * @param $sortkey    The column number to sort by, or '' or null for no sorting
     * @param $sortdir    The direction in which to sort the specified column ('asc' or 'desc')
     * @param $page       The page of the relation to retrieve
     * @param $page_size  The number of rows per page
     * @param &$max_pages (return-by-ref) The max number of pages in the relation
     * @return A recordset on success
     * @return -1 transaction error
     * @return -2 counting error
     * @return -3 page or page_size invalid
     * @return -4 unknown type
     * @return -5 failed setting transaction read only
     */
    public function browseQuery($type, $table, $query, $sortkey, $sortdir, $page, $page_size, &$max_pages)
    {
        // Check that we're not going to divide by zero
        if (!is_numeric($page_size) || $page_size != (int) $page_size || $page_size <= 0) {
            return -3;
        }

        // If $type is TABLE, then generate the query
        switch ($type) {
            case 'TABLE':
                if (preg_match('/^[0-9]+$/', $sortkey) && $sortkey > 0) {
                    $orderby = [$sortkey => $sortdir];
                } else {
                    $orderby = [];
                }

                $query = $this->getSelectSQL($table, [], [], [], $orderby);
                break;
            case 'QUERY':
            case 'SELECT':
                // Trim query
                $query = trim($query);
                // Trim off trailing semi-colon if there is one
                if (substr($query, strlen($query) - 1, 1) == ';') {
                    $query = substr($query, 0, strlen($query) - 1);
                }

                break;
            default:
                return -4;
        }

        // Generate count query
        $count = "SELECT COUNT(*) AS total FROM ($query) AS sub";

        // Open a transaction
        $status = $this->beginTransaction();
        if ($status != 0) {
            return -1;
        }

        // If backend supports read only queries, then specify read only mode
        // to avoid side effects from repeating queries that do writes.
        if ($this->hasReadOnlyQueries()) {
            $status = $this->execute('SET TRANSACTION READ ONLY');
            if ($status != 0) {
                $this->rollbackTransaction();

                return -5;
            }
        }

        // Count the number of rows
        $total = $this->browseQueryCount($query, $count);
        if ($total < 0) {
            $this->rollbackTransaction();

            return -2;
        }

        // Calculate max pages
        $max_pages = ceil($total / $page_size);

        // Check that page is less than or equal to max pages
        if (!is_numeric($page) || $page != (int) $page || $page > $max_pages || $page < 1) {
            $this->rollbackTransaction();

            return -3;
        }

        // Set fetch mode to NUM so that duplicate field names are properly returned
        // for non-table queries.  Since the SELECT feature only allows selecting one
        // table, duplicate fields shouldn't appear.
        if ($type == 'QUERY') {
            $this->conn->setFetchMode(ADODB_FETCH_NUM);
        }

        // Figure out ORDER BY.  Sort key is always the column number (based from one)
        // of the column to order by.  Only need to do this for non-TABLE queries
        if ($type != 'TABLE' && preg_match('/^[0-9]+$/', $sortkey) && $sortkey > 0) {
            $orderby = " ORDER BY {$sortkey}";
            // Add sort order
            if ($sortdir == 'desc') {
                $orderby .= ' DESC';
            } else {
                $orderby .= ' ASC';
            }
        } else {
            $orderby = '';
        }

        // Actually retrieve the rows, with offset and limit
        $rs     = $this->selectSet("SELECT * FROM ({$query}) AS sub {$orderby} LIMIT {$page_size} OFFSET " . ($page - 1) * $page_size);
        $status = $this->endTransaction();
        if ($status != 0) {
            $this->rollbackTransaction();

            return -1;
        }

        return $rs;
    }

    /**
     * Generates the SQL for the 'select' function
     *
     * @param $table   The table from which to select
     * @param $show    An array of columns to show.  Empty array means all columns.
     * @param $values  An array mapping columns to values
     * @param $ops     An array of the operators to use
     * @param $orderby (optional) An array of column numbers or names (one based)
     *                 mapped to sort direction (asc or desc or '' or null) to order by
     * @return The SQL query
     */
    public function getSelectSQL($table, $show, $values, $ops, $orderby = [])
    {
        $this->fieldArrayClean($show);

        // If an empty array is passed in, then show all columns
        if (sizeof($show) == 0) {
            if ($this->hasObjectID($table)) {
                $sql = "SELECT \"{$this->id}\", * FROM ";
            } else {
                $sql = 'SELECT * FROM ';
            }
        } else {
            // Add oid column automatically to results for editing purposes
            if (!in_array($this->id, $show) && $this->hasObjectID($table)) {
                $sql = "SELECT \"{$this->id}\", \"";
            } else {
                $sql = 'SELECT "';
            }

            $sql .= join('","', $show) . '" FROM ';
        }

        $this->fieldClean($table);

        if (isset($_REQUEST['schema'])) {
            $f_schema = $_REQUEST['schema'];
            $this->fieldClean($f_schema);
            $sql .= "\"{$f_schema}\".";
        }
        $sql .= "\"{$table}\"";

        // If we have values specified, add them to the WHERE clause
        $first = true;
        if (is_array($values) && sizeof($values) > 0) {
            foreach ($values as $k => $v) {
                if ($v != '' || $this->selectOps[$ops[$k]] == 'p') {
                    $this->fieldClean($k);
                    if ($first) {
                        $sql .= ' WHERE ';
                        $first = false;
                    } else {
                        $sql .= ' AND ';
                    }
                    // Different query format depending on operator type
                    switch ($this->selectOps[$ops[$k]]) {
                        case 'i':
                            // Only clean the field for the inline case
                            // this is because (x), subqueries need to
                            // to allow 'a','b' as input.
                            $this->clean($v);
                            $sql .= "\"{$k}\" {$ops[$k]} '{$v}'";
                            break;
                        case 'p':
                            $sql .= "\"{$k}\" {$ops[$k]}";
                            break;
                        case 'x':
                            $sql .= "\"{$k}\" {$ops[$k]} ({$v})";
                            break;
                        case 't':
                            $sql .= "\"{$k}\" {$ops[$k]}('{$v}')";
                            break;
                        default:
                            // Shouldn't happen
                    }
                }
            }
        }

        // ORDER BY
        if (is_array($orderby) && sizeof($orderby) > 0) {
            $sql .= ' ORDER BY ';
            $first = true;
            foreach ($orderby as $k => $v) {
                if ($first) {
                    $first = false;
                } else {
                    $sql .= ', ';
                }

                if (preg_match('/^[0-9]+$/', $k)) {
                    $sql .= $k;
                } else {
                    $this->fieldClean($k);
                    $sql .= '"' . $k . '"';
                }
                if (strtoupper($v) == 'DESC') {
                    $sql .= ' DESC';
                }
            }
        }

        return $sql;
    }

    public function hasReadOnlyQueries()
    {
        return true;
    }

    /**
     * Finds the number of rows that would be returned by a
     * query.
     *
     * @param $query The SQL query
     * @param $count The count query
     * @return The count of rows
     * @return -1 error
     */
    public function browseQueryCount($query, $count)
    {
        return $this->selectField($count, 'total');
    }

    /**
     * Returns a recordset of all columns in a table
     *
     * @param $table The name of a table
     * @param $key   The associative array holding the key to retrieve
     * @return A recordset
     */
    public function browseRow($table, $key)
    {
        $f_schema = $this->_schema;
        $this->fieldClean($f_schema);
        $this->fieldClean($table);

        $sql = "SELECT * FROM \"{$f_schema}\".\"{$table}\"";
        if (is_array($key) && sizeof($key) > 0) {
            $sql .= ' WHERE true';
            foreach ($key as $k => $v) {
                $this->fieldClean($k);
                $this->clean($v);
                $sql .= " AND \"{$k}\"='{$v}'";
            }
        }

        return $this->selectSet($sql);
    }

    /**
     * Change the value of a parameter to 't' or 'f' depending on whether it evaluates to true or false
     *
     * @param $parameter the parameter
     * @return \PHPPgAdmin\Database\the|string
     */
    public function dbBool(&$parameter)
    {
        if ($parameter) {
            $parameter = 't';
        } else {
            $parameter = 'f';
        }

        return $parameter;
    }

    /**
     * Fetches statistics for a database
     *
     * @param $database The database to fetch stats for
     * @return A recordset
     */
    public function getStatsDatabase($database)
    {
        $this->clean($database);

        $sql = "SELECT * FROM pg_stat_database WHERE datname='{$database}'";

        return $this->selectSet($sql);
    }

    /**
     * Fetches tuple statistics for a table
     *
     * @param $table The table to fetch stats for
     * @return A recordset
     */
    public function getStatsTableTuples($table)
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $this->clean($table);

        $sql = "SELECT * FROM pg_stat_all_tables
			WHERE schemaname='{$c_schema}' AND relname='{$table}'";

        return $this->selectSet($sql);
    }

    /**
     * Fetches I/0 statistics for a table
     *
     * @param $table The table to fetch stats for
     * @return A recordset
     */
    public function getStatsTableIO($table)
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $this->clean($table);

        $sql = "SELECT * FROM pg_statio_all_tables
			WHERE schemaname='{$c_schema}' AND relname='{$table}'";

        return $this->selectSet($sql);
    }

    /**
     * Fetches tuple statistics for all indexes on a table
     *
     * @param $table The table to fetch index stats for
     * @return A recordset
     */
    public function getStatsIndexTuples($table)
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $this->clean($table);

        $sql = "SELECT * FROM pg_stat_all_indexes
			WHERE schemaname='{$c_schema}' AND relname='{$table}' ORDER BY indexrelname";

        return $this->selectSet($sql);
    }

    /**
     * Fetches I/0 statistics for all indexes on a table
     *
     * @param $table The table to fetch index stats for
     * @return A recordset
     */
    public function getStatsIndexIO($table)
    {
        $c_schema = $this->_schema;
        $this->clean($c_schema);
        $this->clean($table);

        $sql = "SELECT * FROM pg_statio_all_indexes
			WHERE schemaname='{$c_schema}' AND relname='{$table}'
			ORDER BY indexrelname";

        return $this->selectSet($sql);
    }

    public function hasAggregateSortOp()
    {
        return true;
    }

    public function hasAlterAggregate()
    {
        return true;
    }

    public function hasAlterColumnType()
    {
        return true;
    }

    public function hasAlterDatabaseOwner()
    {
        return true;
    }

    public function hasAlterSchema()
    {
        return true;
    }

    public function hasAlterSchemaOwner()
    {
        return true;
    }

    public function hasAlterSequenceSchema()
    {
        return true;
    }

    public function hasAlterSequenceStart()
    {
        return true;
    }

    public function hasAlterTableSchema()
    {
        return true;
    }

    public function hasAutovacuum()
    {
        return true;
    }

    public function hasCreateTableLike()
    {
        return true;
    }

    public function hasDisableTriggers()
    {
        return true;
    }

    public function hasAlterDomains()
    {
        return true;
    }

    public function hasEnumTypes()
    {
        return true;
    }

    public function hasFTS()
    {
        return true;
    }

    public function hasFunctionCosting()
    {
        return true;
    }

    public function hasFunctionGUC()
    {
        return true;
    }

    public function hasNamedParams()
    {
        return true;
    }

    public function hasPrepare()
    {
        return true;
    }

    public function hasPreparedXacts()
    {
        return true;
    }

    public function hasRecluster()
    {
        return true;
    }

    public function hasServerAdminFuncs()
    {
        return true;
    }

    public function hasQueryCancel()
    {
        return true;
    }

    public function hasUserRename()
    {
        return true;
    }

    public function hasUserSignals()
    {
        return true;
    }

    public function hasVirtualTransactionId()
    {
        return true;
    }

    public function hasAlterDatabase()
    {
        return $this->hasAlterDatabaseRename();
    }

    public function hasAlterDatabaseRename()
    {
        return true;
    }

    public function hasDatabaseCollation()
    {
        return true;
    }

    public function hasMagicTypes()
    {
        return true;
    }

    public function hasQueryKill()
    {
        return true;
    }

    public function hasConcurrentIndexBuild()
    {
        return true;
    }

    public function hasForceReindex()
    {
        return false;
    }

    public function hasByteaHexDefault()
    {
        return true;
    }

}
